CIL解析Double类型字符串"15"出现精度偏差的原因咨询
浮点数解析精度差异问题:C#与手动编写CIL的行为不一致
我同时实现了C#版本的解析器和手动编写的CIL解析器,发现一个异常现象:
- C#代码将字符串
"15"解析为double类型时,结果是15 - 手动编写的CIL代码解析同一个字符串,得到的结果却是
15.000000000000004
测试中,"+"运算的用例能通过,但"*"、"-"、"/"运算的测试会出现预期值15和实际值15.000000000000004不匹配的问题。我猜测C#编译为CIL时会对浮点数精度做特殊处理,但手动编写的CIL直接转机器码时没有额外转换,想请教这个现象的原因和解决办法。
CIL代码
.method private static valuetype Hw2.CalculatorOperation ParseOperation(string arg) cil managed { ldarg.0 ldstr "+" call bool [System.Runtime]System.String::op_Equality(string, string) brtrue.s PlusCase ldarg.0 ldstr "-" call bool [System.Runtime]System.String::op_Equality(string, string) brtrue.s MinusCase ldarg.0 ldstr "/" call bool [System.Runtime]System.String::op_Equality(string, string) brtrue.s DivideCase ldarg.0 ldstr "*" call bool [System.Runtime]System.String::op_Equality(string, string) brtrue.s MultiplyCase ldc.i4.4 ret PlusCase: ldc.i4.0 ret MinusCase: ldc.i4.1 ret MultiplyCase: ldc.i4.2 ret DivideCase: ldc.i4.3 ret } .method private static bool IsArgLengthSupported(string[] args) cil managed { ldarg.0 ldlen ldc.i4.3 ceq ret } .method public static void ParseCalcArguments(string[] args, [out] float64& val1, [out] valuetype Hw2.CalculatorOperation& op, [out] float64& val2) cil managed { ldarg.0 call bool Hw2.Parser::IsArgLengthSupported(string[]) ldc.i4.0 beq.s ArgumentException ldarg.0 ldc.i4.1 ldelem.ref call valuetype Hw2.CalculatorOperation Hw2.Parser::ParseOperation(string) ldc.i4.4 ceq brtrue InvalidOperationException ldarg.0 ldc.i4.0 ldelem.ref ldarg.1 call bool [System.Runtime]System.Double::TryParse(string, float64&) brtrue.s SecondTryParse ldstr "first number isn't actually a number" newobj instance void [System.Runtime]System.ArgumentException::.ctor(string) throw SecondTryParse: ldarg.0 ldc.i4.2 ldelem.ref ldarg.3 call bool [System.Runtime]System.Double::TryParse(string, float64&) brtrue.s CalculationOperationDefining ldstr "second number isn't actually a number" newobj instance void [System.Runtime]System.ArgumentException::.ctor(string) throw CalculationOperationDefining: ldarg.1 ldarg.0 ldc.i4.1 ldelem.ref call valuetype Hw2.CalculatorOperation Hw2.Parser::ParseOperation(string) stind.i4 ret InvalidOperationException: ldstr "unknown operation" newobj instance void [System.Runtime]System.InvalidOperationException::.ctor(string) throw ArgumentException: ldstr "array length should be 3" newobj instance void [System.Runtime]System.ArgumentException::.ctor(string) throw }
测试代码
[InlineData("*", CalculatorOperation.Multiply)] public void TestCorrectOperations(string operation, CalculatorOperation operationExpected) { // arrange var args = new[] { "15", operation, "5" }; //act Parser.ParseCalcArguments(args, out var val1, out var operationResult, out var val2); //assert Assert.Equal(15, val1); Assert.Equal(operationExpected, operationResult); Assert.Equal(5, val2); }
C#解析器代码
public static void ParseCalcArguments(string[] args, out double val1, out CalculatorOperation operation, out double val2) { if (!IsArgLengthSupported(args)) throw new ArgumentException("array length should be 3"); if (ParseOperation(args[1]) == CalculatorOperation.Undefined) throw new InvalidOperationException(); if (Double.TryParse(args[0], out val1) && Double.TryParse(args[2], out val2)) { operation = ParseOperation(args[1]); } else throw new ArgumentException("one of the numbers isn't actually a number"); }
问题原因与解决办法
核心原因
你编写的CIL代码存在逻辑错误:在CalculationOperationDefining代码块中,错误地将解析出的操作码写入了val1的输出参数(ldarg.1是val1的引用),而不是目标参数op(对应ldarg.2)。
这个错误会导致已经解析好的double类型val1的内存被覆盖成操作码的整数值(比如乘法操作是2),当double类型读取这段内存时,会解析出一个接近15的异常数值,也就是你看到的15.000000000000004。
解决办法
修复CIL代码的参数错误:
修改CalculationOperationDefining部分的代码,将操作码写入正确的op参数:CalculationOperationDefining: ldarg.2 // 替换原有的ldarg.1,指向op参数 ldarg.0 ldc.i4.1 ldelem.ref call valuetype Hw2.CalculatorOperation Hw2.Parser::ParseOperation(string) stind.i4 ret规范浮点数断言方式:
即使修复了逻辑错误,后续处理浮点数时也不应该用精确相等比较。在测试中使用带容差的断言:// 指定精度位数 Assert.Equal(15, val1, precision: 10); // 或者直接设置容差值 Assert.Equal(15, val1, 1e-9);
内容的提问来源于stack exchange,提问作者Nathan Ikazuyir
相关产品推荐
相关产品推荐

