BenchmarkDotNet icon indicating copy to clipboard operation
BenchmarkDotNet copied to clipboard

Compilation fails with Literal of type double cannot be implicitly converted.

Open redknightlois opened this issue 8 months ago • 4 comments

After updating to the latest .Net 9.0 SDK and updating Visual Studio we started having issues compiling the solutions. Since we had similar issues on our codebase, maybe it is related to the changes in overload resolution.

S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\e60f7001-ab2b-4959-8145-809b29eb272a.notcs(17761,69): error CS0664: Literal of type double cannot be implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type [S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\BenchmarkDotNet.Autogenerated.csproj]
S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\e60f7001-ab2b-4959-8145-809b29eb272a.notcs(16375,67): error CS0664: Literal of type double cannot be implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type [S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\BenchmarkDotNet.Autogenerated.csproj]
S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\e60f7001-ab2b-4959-8145-809b29eb272a.notcs(17167,69): error CS0664: Literal of type double cannot be implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type [S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\BenchmarkDotNet.Autogenerated.csproj]
S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\e60f7001-ab2b-4959-8145-809b29eb272a.notcs(16573,67): error CS0664: Literal of type double cannot be implicitly converted to type 'float'; use an 'F' suffix to create a literal of this type [S:\Src\ravendb-60-git\bench\Micro.Benchmark\bin\Release\net9.0\e60f7001-ab2b-4959-8145-809b29eb272a\BenchmarkDotNet.Autogenerated.csproj]

You can inspect the attached file. c926cbed-3cb6-4e22-80b1-08aa98602f89.zip

redknightlois avatar Mar 13 '25 18:03 redknightlois

Can you please share your benchmark that caused this failure? Micro.Benchmark.Benchmarks.Parsing.FindEscapePositions

timcassell avatar Mar 14 '25 20:03 timcassell

Sure. This is the benchmark

using System;
using System.Linq;
using BenchmarkDotNet.Analysers;
using BenchmarkDotNet.Attributes;
using BenchmarkDotNet.Configs;
using BenchmarkDotNet.Environments;
using BenchmarkDotNet.Jobs;
using BenchmarkDotNet.Validators;


namespace Micro.Benchmark.Benchmarks.Parsing
{
    //[HardwareCounters(HardwareCounter.CacheMisses, HardwareCounter.TotalIssues, HardwareCounter.BranchMispredictions, HardwareCounter.InstructionRetired )]
    [DisassemblyDiagnoser]
    [Config(typeof(FindEscapePositions.Config))]
    public unsafe class FindEscapePositions
    {
        private class Config : ManualConfig
        {
            public Config()
            {
                AddJob(new Job(RunMode.Default)
                {
                    Environment =
                    {
                        Runtime = CoreRuntime.Core90, 
                        Platform = Platform.X64, 
                        Jit = Jit.RyuJit
                    }
                });

                AddExporter(GetExporters().ToArray());

                AddValidator(BaselineValidator.FailOnError);
                AddValidator(JitOptimizationsValidator.FailOnError);

                AddAnalyser(EnvironmentAnalyser.Default);
            }
        }

        private const int MaxSize = 4096 * 64;

        [Params(7, 16, 127, 128, 255, 256, 4096, MaxSize)]
        public int Length { get; set; }

        [Params(0.0, 0.05, 0.1, 0.5, 0.99, 1.0)]
        public float NonAsciiProbability { get; set; }

        public const int Operations = 100;

        private readonly char[] _source = new char[MaxSize];

        [GlobalSetup]
        public void Setup()
        {
            var r = new Random();
            for (int i = 0; i < MaxSize; i++)
            {
                if (r.NextSingle() < NonAsciiProbability)
                {
                    _source[i] = (char)(r.Next(char.MaxValue - 256) + 256);
                }
                else
                {
                    _source[i] = (char)r.Next(255);
                }
            }
        }

        [Benchmark(Baseline = true)]
        public int Reference()
        {
            var count = 0;
            var controlCount = 0;

            for (int i = 0; i < _source.Length; i++)
            {
                var value = _source[i];

                // PERF: We use the values directly because it is 5x faster than iterating over a constant array.
                // 8  => '\b' => 0000 1000
                // 9  => '\t' => 0000 1001
                // 10 => '\n' => 0000 1010

                // 12 => '\f' => 0000 1100
                // 13 => '\r' => 0000 1101

                // 34 => '"'  => 0010 0010
                // 92 => '\\' => 0101 1100

                if (value == 92 || value == 34 || (value >= 8 && value <= 13 && value != 11))
                {
                    count++;
                    continue;
                }

                if (value < 32)
                {
                    controlCount++;
                }
            }

            // we take 5 because that is the max number of bytes for variable size int
            // plus 1 for the actual number of positions

            // NOTE: this is used by FindEscapePositionsIn, change only if you also modify FindEscapePositionsIn
            return (count + 1) * 5 + controlCount * 5;
        }


        private static ReadOnlySpan<int> EscapePositionsCountTable =>
        [
            0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 0, 1, 1, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        ];

        private static ReadOnlySpan<int> EscapePositionsControlTable =>
        [
            1, 1, 1, 1, 1, 1, 1, 1, 0, 0, 0, 1, 0, 0, 1, 1,
            1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
            0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0
        ];

        [Benchmark]
        public int TableBased()
        {
            var count = 0;
            var controlCount = 0;

            foreach (var value in _source)
            {
                if (value >= byte.MaxValue)
                    continue;

                count += EscapePositionsCountTable[value];
                controlCount += EscapePositionsControlTable[value];
            }

            // we take 5 because that is the max number of bytes for variable size int
            // plus 1 for the actual number of positions

            // NOTE: this is used by FindEscapePositionsIn, change only if you also modify FindEscapePositionsIn
            return (count + 1) * 5 + controlCount * 5;
        }
    }
}

redknightlois avatar Mar 14 '25 22:03 redknightlois

Does it work if you change it like this?

-       [Params(0.0, 0.05, 0.1, 0.5, 0.99, 1.0)]
+       [Params(0.0f, 0.05f, 0.1f, 0.5f, 0.99f, 1.0f)]
        public float NonAsciiProbability { get; set; }

timcassell avatar Mar 14 '25 23:03 timcassell

I have been looking on the code before reading this, and yes that works. I was going to workaround it by just converting to double instead.

            instance.Length = 262144;instance.NonAsciiProbability = 0.05d;

redknightlois avatar Mar 15 '25 00:03 redknightlois

@timcassell What do you think of this solution? https://github.com/dotnet/BenchmarkDotNet/pull/2791

pr0s3q avatar Jun 22 '25 18:06 pr0s3q

@timcassell What do you think of this solution? #2791

I don't like it, it subtly hide bugs. We should rather add a validator that ensures the values are assignable to the type.

timcassell avatar Jun 22 '25 20:06 timcassell

@timcassell reverted previous solution. Added parameters type validation as requested

pr0s3q avatar Jun 23 '25 06:06 pr0s3q

Closing as by-design. We report the error the compiler gives. Compiler behavior is too complex for our validators to try to catch this before compiling.

timcassell avatar Jun 29 '25 09:06 timcassell