dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.96k stars 4.65k forks source link

Complex.Sin/Cos regressions in .NET 9 #105997

Open stephentoub opened 1 month ago

stephentoub commented 1 month ago
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Running;
using System.Numerics;

BenchmarkSwitcher.FromAssembly(typeof(Tests).Assembly).Run(args);

[HideColumns("Job", "Error", "StdDev", "Median", "RatioSD")]
public partial class Tests
{
    private Complex _value = new Complex(1, 2);

    [Benchmark]
    public Complex Sin() => Complex.Sin(_value);

    [Benchmark]
    public Complex Cos() => Complex.Cos(_value);
}

This is what I currently see:

Method Toolchain Mean Ratio
Sin net8.0 21.8224 ns 1.00
Sin net9.0 36.2546 ns 1.66
Cos net8.0 21.8679 ns 1.00
Cos net9.0 37.5421 ns 1.72

cc: @tannergooding

tannergooding commented 1 month ago

What OS is this on?

stephentoub commented 1 month ago

Windows 11

dotnet-policy-service[bot] commented 1 month ago

Tagging subscribers to this area: @dotnet/area-system-numerics See info in area-owners.md if you want to be subscribed.

tannergooding commented 1 month ago

This is likely caused by using SinCos, will take a look.

My guess is this is faster on Linux and that Windows is hitting one of the open issues like https://github.com/dotnet/runtime/issues/48776 or https://developercommunity.visualstudio.com/t/MSVCs-sincos-implementation-is-incorrec/10582378#T-ND10697989

Will confirm and likely change the SinCos impl for Windows to just call Sin and Cos independently for now and track getting it back after the above are resolved

JeffreySax commented 1 month ago

Note also that in some cases, .NET 8.0 Math.SinCos produces slightly different (less accurate) results than .NET 9.0.

For example, for x = 1.57 (different from pi/2 by about 0.0007), the results are:

.NET 8.0:

sin x = 0.9999996829318346
cos x = 0.0007963267107331026

.NET 9.0 (preview 6):

sin x = 0.9999996829318346
cos x = 0.0007963267107332633

Calling Math.Sin and Math.Cos separately gives the same result as .NET 9.0.

tannergooding commented 1 month ago

Note also that in some cases, .NET 8.0 Math.SinCos produces slightly different (less accurate) results than .NET 9.0.

This is https://developercommunity.visualstudio.com/t/MSVCs-sincos-implementation-is-incorrec/10582378#T-ND10697989, which I linked above.


@stephentoub, I'm not able to reproduce this; I also tested on my Intel 11900 and saw similar results

.NET 8 - AVX2

BenchmarkDotNet v0.13.13-nightly.20240311.145, Windows 11 (10.0.26100.1301) AMD Ryzen 9 7950X, 1 CPU, 32 logical and 16 physical cores .NET SDK 9.0.100-preview.6.24328.19 [Host] : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2 Job-GCZARY : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1

Method value Mean Error StdDev Median Min Max Allocated
SinStephen ? 10.073 ns 0.1431 ns 0.1339 ns 10.045 ns 9.920 ns 10.282 ns -
CosStephen ? 9.587 ns 0.0693 ns 0.0649 ns 9.552 ns 9.522 ns 9.720 ns -
Cos <0; 1> 6.704 ns 0.0288 ns 0.0240 ns 6.713 ns 6.656 ns 6.725 ns -
Sin <0; 1> 6.862 ns 0.0347 ns 0.0325 ns 6.842 ns 6.830 ns 6.924 ns -
Cos <1.23456789; 1.23456789> 9.964 ns 0.0569 ns 0.0532 ns 9.975 ns 9.900 ns 10.040 ns -
Sin <1.23456789; 1.23456789> 10.281 ns 0.0434 ns 0.0406 ns 10.278 ns 10.184 ns 10.349 ns -
Cos <1; 0> 9.254 ns 0.0466 ns 0.0436 ns 9.261 ns 9.193 ns 9.323 ns -
Sin <1; 0> 9.256 ns 0.0521 ns 0.0462 ns 9.235 ns 9.210 ns 9.364 ns -
Cos <1; 1> 9.949 ns 0.0587 ns 0.0550 ns 9.926 ns 9.879 ns 10.043 ns -
Sin <1; 1> 9.922 ns 0.0573 ns 0.0536 ns 9.893 ns 9.869 ns 10.040 ns -
Cos <1; 2> 10.342 ns 0.0506 ns 0.0474 ns 10.320 ns 10.284 ns 10.441 ns -
Sin <1; 2> 9.872 ns 0.0485 ns 0.0453 ns 9.871 ns 9.804 ns 9.947 ns -

.NET 8 - AVX512

BenchmarkDotNet v0.13.13-nightly.20240311.145, Windows 11 (10.0.26100.1301) AMD Ryzen 9 7950X, 1 CPU, 32 logical and 16 physical cores .NET SDK 9.0.100-preview.6.24328.19 [Host] : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI Job-JGPDSN : .NET 8.0.5 (8.0.524.21615), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1

Method value Mean Error StdDev Median Min Max Allocated
SinStephen ? 9.980 ns 0.1542 ns 0.1443 ns 9.957 ns 9.810 ns 10.177 ns -
CosStephen ? 9.777 ns 0.0737 ns 0.0689 ns 9.758 ns 9.684 ns 9.909 ns -
Cos <0; 1> 6.687 ns 0.0433 ns 0.0405 ns 6.691 ns 6.632 ns 6.743 ns -
Sin <0; 1> 6.627 ns 0.0450 ns 0.0421 ns 6.600 ns 6.580 ns 6.695 ns -
Cos <1.23456789; 1.23456789> 10.395 ns 0.0501 ns 0.0469 ns 10.414 ns 10.310 ns 10.436 ns -
Sin <1.23456789; 1.23456789> 9.851 ns 0.0628 ns 0.0587 ns 9.856 ns 9.767 ns 9.951 ns -
Cos <1; 0> 9.139 ns 0.0486 ns 0.0455 ns 9.143 ns 9.074 ns 9.203 ns -
Sin <1; 0> 9.650 ns 0.0544 ns 0.0509 ns 9.648 ns 9.580 ns 9.743 ns -
Cos <1; 1> 10.348 ns 0.0433 ns 0.0405 ns 10.352 ns 10.247 ns 10.405 ns -
Sin <1; 1> 10.031 ns 0.0619 ns 0.0579 ns 10.036 ns 9.964 ns 10.128 ns -
Cos <1; 2> 10.340 ns 0.0674 ns 0.0630 ns 10.359 ns 10.250 ns 10.415 ns -
Sin <1; 2> 10.152 ns 0.0239 ns 0.0187 ns 10.143 ns 10.133 ns 10.186 ns -

.NET 9 - AVX2

BenchmarkDotNet v0.13.13-nightly.20240311.145, Windows 11 (10.0.26100.1301) AMD Ryzen 9 7950X, 1 CPU, 32 logical and 16 physical cores .NET SDK 9.0.100-preview.6.24328.19 [Host] : .NET 9.0.0 (9.0.24.32707), X64 RyuJIT AVX2 Job-IXTZQT : .NET 9.0.0 (9.0.24.32707), X64 RyuJIT AVX2

PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1

Method value Mean Error StdDev Median Min Max Allocated
SinStephen ? 9.911 ns 0.1471 ns 0.1304 ns 9.866 ns 9.796 ns 10.186 ns -
CosStephen ? 10.133 ns 0.0711 ns 0.0665 ns 10.125 ns 10.023 ns 10.242 ns -
Cos <0; 1> 6.869 ns 0.0546 ns 0.0511 ns 6.868 ns 6.786 ns 6.938 ns -
Sin <0; 1> 6.868 ns 0.0501 ns 0.0444 ns 6.870 ns 6.811 ns 6.937 ns -
Cos <1.23456789; 1.23456789> 10.323 ns 0.0657 ns 0.0615 ns 10.326 ns 10.227 ns 10.414 ns -
Sin <1.23456789; 1.23456789> 10.146 ns 0.0598 ns 0.0559 ns 10.149 ns 10.063 ns 10.240 ns -
Cos <1; 0> 9.293 ns 0.1028 ns 0.0911 ns 9.277 ns 9.180 ns 9.465 ns -
Sin <1; 0> 9.634 ns 0.1120 ns 0.1048 ns 9.623 ns 9.525 ns 9.854 ns -
Cos <1; 1> 10.285 ns 0.0676 ns 0.0632 ns 10.248 ns 10.216 ns 10.416 ns -
Sin <1; 1> 9.844 ns 0.0584 ns 0.0546 ns 9.860 ns 9.767 ns 9.933 ns -
Cos <1; 2> 10.103 ns 0.0757 ns 0.0708 ns 10.086 ns 10.023 ns 10.229 ns -
Sin <1; 2> 10.200 ns 0.0905 ns 0.0847 ns 10.201 ns 10.089 ns 10.306 ns -

.NET 9 - AVX512

BenchmarkDotNet v0.13.13-nightly.20240311.145, Windows 11 (10.0.26100.1301) AMD Ryzen 9 7950X, 1 CPU, 32 logical and 16 physical cores .NET SDK 9.0.100-preview.6.24328.19 [Host] : .NET 9.0.0 (9.0.24.32707), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI Job-EAJRXI : .NET 9.0.0 (9.0.24.32707), X64 RyuJIT AVX-512F+CD+BW+DQ+VL+VBMI

PowerPlanMode=00000000-0000-0000-0000-000000000000 IterationTime=250ms MaxIterationCount=20 MinIterationCount=15 WarmupCount=1

Method value Mean Error StdDev Median Min Max Allocated
SinStephen ? 10.247 ns 0.0718 ns 0.0561 ns 10.248 ns 10.171 ns 10.333 ns -
CosStephen ? 10.254 ns 0.0503 ns 0.0471 ns 10.248 ns 10.172 ns 10.335 ns -
Cos <0; 1> 6.876 ns 0.0306 ns 0.0287 ns 6.870 ns 6.823 ns 6.924 ns -
Sin <0; 1> 6.701 ns 0.0380 ns 0.0356 ns 6.704 ns 6.634 ns 6.747 ns -
Cos <1.23456789; 1.23456789> 10.075 ns 0.0737 ns 0.0689 ns 10.088 ns 9.972 ns 10.158 ns -
Sin <1.23456789; 1.23456789> 10.189 ns 0.0825 ns 0.0772 ns 10.186 ns 10.099 ns 10.376 ns -
Cos <1; 0> 9.596 ns 0.0541 ns 0.0452 ns 9.596 ns 9.521 ns 9.677 ns -
Sin <1; 0> 9.475 ns 0.0490 ns 0.0459 ns 9.484 ns 9.403 ns 9.541 ns -
Cos <1; 1> 10.464 ns 0.2140 ns 0.2002 ns 10.523 ns 10.065 ns 10.776 ns -
Sin <1; 1> 10.195 ns 0.0693 ns 0.0649 ns 10.204 ns 10.079 ns 10.286 ns -
Cos <1; 2> 10.296 ns 0.0640 ns 0.0568 ns 10.281 ns 10.238 ns 10.406 ns -
Sin <1; 2> 10.161 ns 0.0638 ns 0.0597 ns 10.158 ns 10.083 ns 10.249 ns -
tannergooding commented 1 month ago

This was using:

// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Collections.Generic;
using System.Linq;
using BenchmarkDotNet.Attributes;
using MicroBenchmarks;

namespace System.Numerics.Tests
{
    [BenchmarkCategory(Categories.Libraries)]
    public class Perf_Complex
    {
        public IEnumerable<object> Values()
        {
            yield return new Complex(1.0, 0.0);
            yield return new Complex(0.0, 1.0);
            yield return new Complex(1.0, 1.0);
            yield return new Complex(1.23456789, 1.23456789);
            yield return new Complex(1, 2);
        }

        [Benchmark]
        [ArgumentsSource(nameof(Values))]
        public Complex Cos(Complex value) => Complex.Cos(value);

        [Benchmark]
        [ArgumentsSource(nameof(Values))]
        public Complex Sin(Complex value) => Complex.Sin(value);

        private Complex _value = new Complex(1, 2);

        [Benchmark]
        public Complex SinStephen() => Complex.Sin(_value);

        [Benchmark]
        public Complex CosStephen() => Complex.Cos(_value);
    }
}

which I was working on adding to the dotnet/performance repo (wasn't planning on adding SinStephen/CosStephen, just added them locally when I couldn't repro initially)

stephentoub commented 1 month ago

This was using:

I get this:

Method Runtime value Mean Ratio
SinStephen .NET 8.0 ? 22.17 ns 1.00
SinStephen .NET 9.0 ? 38.29 ns 1.75
CosStephen .NET 8.0 ? 22.21 ns 1.00
CosStephen .NET 9.0 ? 38.56 ns 1.74
Cos .NET 8.0 <0; 1> 14.54 ns 1.00
Cos .NET 9.0 <0; 1> 30.64 ns 2.11
Sin .NET 8.0 <0; 1> 15.04 ns 1.00
Sin .NET 9.0 <0; 1> 29.67 ns 1.97
Cos .NET 8.0 <1.23(...)6789> [24] 22.05 ns 1.00
Cos .NET 9.0 <1.23(...)6789> [24] 38.88 ns 1.76
Sin .NET 8.0 <1.23(...)6789> [24] 21.94 ns 1.00
Sin .NET 9.0 <1.23(...)6789> [24] 38.44 ns 1.75
Cos .NET 8.0 <1; 0> 17.79 ns 1.00
Cos .NET 9.0 <1; 0> 20.97 ns 1.18
Sin .NET 8.0 <1; 0> 17.61 ns 1.00
Sin .NET 9.0 <1; 0> 20.71 ns 1.18
Cos .NET 8.0 <1; 1> 22.11 ns 1.00
Cos .NET 9.0 <1; 1> 38.64 ns 1.75
Sin .NET 8.0 <1; 1> 22.11 ns 1.00
Sin .NET 9.0 <1; 1> 37.71 ns 1.71
Cos .NET 8.0 <1; 2> 22.15 ns 1.00
Cos .NET 9.0 <1; 2> 38.40 ns 1.73
Sin .NET 8.0 <1; 2> 22.12 ns 1.00
Sin .NET 9.0 <1; 2> 38.09 ns 1.72

on this configuration:

BenchmarkDotNet v0.13.12, Windows 11 (10.0.22631.3958/23H2/2023Update/SunValley3)
Intel Core i7-8700 CPU 3.20GHz (Coffee Lake), 1 CPU, 12 logical and 6 physical cores
.NET SDK 9.0.100-preview.7.24373.4
  [Host]     : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
  Job-WRZLJY : .NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2
  Job-GMITOL : .NET 9.0.0 (9.0.24.36618), X64 RyuJIT AVX2

I tried again with a runtime built locally from latest in main, and I get basically the same thing:

Method Toolchain value Mean Ratio
SinStephen net8.0 ? 21.62 ns 1.00
SinStephen CoreRun ? 37.38 ns 1.73
CosStephen net8.0 ? 21.90 ns 1.00
CosStephen CoreRun ? 37.69 ns 1.72
Cos net8.0 <0; 1> 14.64 ns 1.00
Cos CoreRun <0; 1> 30.49 ns 2.08
Sin net8.0 <0; 1> 15.44 ns 1.00
Sin CoreRun <0; 1> 29.71 ns 1.92
Cos net8.0 <1.23(...)6789> [24] 21.77 ns 1.00
Cos CoreRun <1.23(...)6789> [24] 37.52 ns 1.72
Sin net8.0 <1.23(...)6789> [24] 21.78 ns 1.00
Sin CoreRun <1.23(...)6789> [24] 37.36 ns 1.71
Cos net8.0 <1; 0> 17.74 ns 1.00
Cos CoreRun <1; 0> 19.97 ns 1.13
Sin net8.0 <1; 0> 17.73 ns 1.00
Sin CoreRun <1; 0> 20.33 ns 1.15
Cos net8.0 <1; 1> 22.13 ns 1.00
Cos CoreRun <1; 1> 38.48 ns 1.75
Sin net8.0 <1; 1> 21.97 ns 1.00
Sin CoreRun <1; 1> 37.89 ns 1.72
Cos net8.0 <1; 2> 21.94 ns 1.00
Cos CoreRun <1; 2> 38.09 ns 1.73
Sin net8.0 <1; 2> 22.29 ns 1.00
Sin CoreRun <1; 2> 38.17 ns 1.71
tannergooding commented 1 month ago

Could you get the disassembly for the runs here? On my box, the codegen is nearly identical, the only difference is that .NET 8 has a vzeroupper emitted.

So, either you're getting pessimized due to lack of vzeroupper or you're likely being pessimized by the JCC erratum: https://www.intel.com/content/www/us/en/developer/articles/technical/software-security-guidance/best-practices/mitigation-strategies-jcc-microcode.html

My guess is you're being impacted by the latter given that you're on Coffee Lake and vzeroupper has been highly optimized since Skylake (plus we're already zeroing the upper bits of all the used registers and Cos+Sin should be executing the internal AVX aware path themselves, so vzeroupper "should" be unnecessary).

stephentoub commented 1 month ago

Via [DisassemblyDiagnoser]

.NET 8.0.7 (8.0.724.31311), X64 RyuJIT AVX2

; Tests.Cos(System.Numerics.Complex)
       push      rbx
       sub       rsp,70
       vzeroupper
       vmovaps   [rsp+60],xmm6
       vmovaps   [rsp+50],xmm7
       vmovaps   [rsp+40],xmm8
       vmovaps   [rsp+30],xmm9
       mov       rbx,rdx
       vmovsd    xmm0,qword ptr [r8]
       vmovsd    qword ptr [rsp+28],xmm0
       vmovsd    xmm1,qword ptr [r8+8]
       vmovaps   xmm0,xmm1
       call      System.Math.Exp(Double)
       vmovaps   xmm6,xmm0
       vmovsd    xmm0,qword ptr [7FFF954A86A0]
       vdivsd    xmm7,xmm0,xmm6
       vsubsd    xmm0,xmm6,xmm7
       vmovsd    xmm8,qword ptr [7FFF954A86A8]
       vmulsd    xmm9,xmm0,xmm8
       vmovsd    xmm0,qword ptr [rsp+28]
       call      System.Math.Cos(Double)
       vaddsd    xmm1,xmm6,xmm7
       vmulsd    xmm1,xmm1,xmm8
       vmulsd    xmm6,xmm0,xmm1
       vmovsd    xmm0,qword ptr [rsp+28]
       call      System.Math.Sin(Double)
       vxorps    xmm0,xmm0,[7FFF954A86B0]
       vmulsd    xmm0,xmm0,xmm9
       vmovsd    qword ptr [rbx],xmm6
       vmovsd    qword ptr [rbx+8],xmm0
       mov       rax,rbx
       vmovaps   xmm6,[rsp+60]
       vmovaps   xmm7,[rsp+50]
       vmovaps   xmm8,[rsp+40]
       vmovaps   xmm9,[rsp+30]
       add       rsp,70
       pop       rbx
       ret
; Total bytes of code 184

Extern method System.Math.Exp(Double) System.Math.Cos(Double) System.Math.Sin(Double)

.NET 9.0.0 (9.0.24.36618), X64 RyuJIT AVX2

; Tests.Cos(System.Numerics.Complex)
       push      rbx
       sub       rsp,60
       vmovaps   [rsp+50],xmm6
       mov       rbx,rdx
       vmovsd    xmm0,qword ptr [r8]
       vmovsd    xmm1,qword ptr [r8+8]
       vmovsd    qword ptr [rsp+30],xmm1
       lea       r8,[rsp+38]
       lea       rdx,[rsp+40]
       call      System.Math.SinCos(Double, Double*, Double*)
       vmovsd    xmm0,qword ptr [rsp+40]
       vmovsd    xmm1,qword ptr [rsp+38]
       vmovsd    qword ptr [rsp+28],xmm0
       vmovsd    qword ptr [rsp+20],xmm1
       vmovsd    xmm0,qword ptr [rsp+30]
       call      System.Math.Cosh(Double)
       vmulsd    xmm6,xmm0,qword ptr [rsp+20]
       vmovsd    xmm1,qword ptr [rsp+28]
       vxorps    xmm1,xmm1,[7FFF8ACA9540]
       vmovsd    qword ptr [rsp+48],xmm1
       vmovsd    xmm0,qword ptr [rsp+30]
       call      System.Math.Sinh(Double)
       vmulsd    xmm0,xmm0,qword ptr [rsp+48]
       vmovsd    qword ptr [rbx],xmm6
       vmovsd    qword ptr [rbx+8],xmm0
       mov       rax,rbx
       vmovaps   xmm6,[rsp+50]
       add       rsp,60
       pop       rbx
       ret
; Total bytes of code 148

Extern method System.Math.SinCos(Double, Double, Double) System.Math.Cosh(Double) System.Math.Sinh(Double)

tannergooding commented 1 month ago

Ah, I see. I was running the benchmark with the wrong 9.0 path and it was picking up an outdated library version. Doing a clean build fixed that and I get:

Method value Mean Error StdDev Median Min Max Code Size Allocated
SinStephen ? 20.26 ns 0.269 ns 0.252 ns 20.21 ns 19.74 ns 20.65 ns 127 B -
CosStephen ? 20.29 ns 0.256 ns 0.239 ns 20.30 ns 19.94 ns 20.59 ns 147 B -
Cos <0; 1> 14.83 ns 0.103 ns 0.091 ns 14.84 ns 14.63 ns 15.01 ns 148 B -
Sin <0; 1> 14.72 ns 0.130 ns 0.121 ns 14.72 ns 14.56 ns 14.93 ns 128 B -
Cos <1.23456789; 1.23456789> 20.13 ns 0.169 ns 0.159 ns 20.13 ns 19.86 ns 20.37 ns 148 B -
Sin <1.23456789; 1.23456789> 19.64 ns 0.041 ns 0.034 ns 19.63 ns 19.60 ns 19.71 ns 128 B -
Cos <1; 0> 15.14 ns 0.149 ns 0.132 ns 15.09 ns 14.99 ns 15.46 ns 148 B -
Sin <1; 0> 29.21 ns 0.678 ns 0.781 ns 29.37 ns 27.11 ns 30.27 ns 128 B -
Cos <1; 1> 19.99 ns 0.161 ns 0.150 ns 20.02 ns 19.79 ns 20.25 ns 148 B -
Sin <1; 1> 20.03 ns 0.130 ns 0.115 ns 20.01 ns 19.83 ns 20.26 ns 128 B -
Cos <1; 2> 19.86 ns 0.049 ns 0.041 ns 19.85 ns 19.81 ns 19.95 ns 148 B -
Sin <1; 2> 19.81 ns 0.148 ns 0.138 ns 19.86 ns 19.62 ns 19.98 ns 128 B -

The reason for the regresion here is that .NET 9 changed from calling:

double p = Math.Exp(value.m_imaginary);
double q = 1.0 / p;
double sinh = (p - q) * 0.5;
double cosh = (p + q) * 0.5;

To instead doing (effectively):

double sinh = Math.Sinh(value.m_imaginary);
double cosh = Math.Cosh(value.m_imaginary);

This can be reproduced in isolation via:

public IEnumerable<object> Values()
{
    yield return new Complex(1.0, 0.0);
    yield return new Complex(0.0, 1.0);
    yield return new Complex(1.0, 1.0);
    yield return new Complex(1.23456789, 1.23456789);
    yield return new Complex(1, 2);
}

[Benchmark]
[ArgumentsSource(nameof(Values))]
public Complex SinNet8(Complex value)
{
    double p = Math.Exp(value.Imaginary);
    double q = 1.0 / p;
    double sinh = (p - q) * 0.5;
    double cosh = (p + q) * 0.5;
    return new Complex(Math.Sin(value.Real) * cosh, Math.Cos(value.Real) * sinh);
}

[Benchmark]
[ArgumentsSource(nameof(Values))]
public Complex SinNet9(Complex value)
{
    (var sin, var cos) = Math.SinCos(value.Real);
    return new Complex(sin * Math.Cosh(value.Imaginary), cos * Math.Sinh(value.Imaginary));
}

[Benchmark]
[ArgumentsSource(nameof(Values))]
public Complex SinNet9Individual(Complex value)
{
    return new Complex(Math.Sin(value.Real) * Math.Cosh(value.Imaginary), Math.Cos(Value.Real) * Math.Sinh(value.Imaginary));
}

The change was made to ensure more correct results, but is more expensive since we're computing the proper sinh and cosh independently. We could introduce some SinhCosh pair to reduce this cost and win that back but that's likely out of scope for .NET 9. I don't think we want to back out the change since its explicitly there to ensure we get more accurate results and correct edge case handling.