Runtime: Question: Should .Net remove dependency on the C runtime?

Created on 15 Jun 2019  路  7Comments  路  Source: dotnet/runtime

This is just a question because I was surprised to learn that .Net does not implement it's own mathematical library. For example Math.Pow() relies on the C runtime which could behave differently on different platforms.

There are in fact two problems with this approach:

  1. Code could behave differently or compute different results on different platforms.
  2. The runtime used by the compiler (at compile time) is not guaranteed to be the same at runtime.

Each of these strikes me as a risk, particularly in compute heavy applications like navigation, machine learning, IoT and so on.

So, do others agree? is it time that the C runtime was dumped and a solid implementation of that same code was created in C# and made part of the BCL? How much effort would this be? could the C runtime source code more or less be "hand translated" into C#?

For those interested I found an example of an implementation source code here.

area-VM-coreclr question

Most helpful comment

For example if it ever became possible to initialize constants by calling pure methods (e.g. Math.Pow) then there is a risk that the values computed during compilation might differ from the values computed at runtime for the same valued arguments.

This would still be an issue for C# though. Because there's no pure def for FP at the CIL level.

If the math libraries were written in C# then the compiler could use the relevant C# math library for the target platform specified for the project being compiled.

Even if they were written in C, the compiler could still use them. THe primary problem is that for something to be pure you need to guarantee that it actually can be pure. This means at the specification level (regardless of impl) you need to limit to things you know will always produce the same results.

--

I get what you're trying to solve. I'm just stating that this isn't really an issue with thins being C or C#. It's an issue with the specification allowing for differing behavior, and you wanting that to combine with a feature that is about knowing strictly what the behavior will be and having that be consistent everywhere. You need to solve the latter bit first.

All 7 comments

Code could behave differently or compute different results on different platforms.

I don't think that's anything about C. C# can behave differently or produce different results on different platforms. Floating point math, for example, is defined at the common runtime level (i.e. above C) in a way that allows for it to produce different results.

A large part of this is that the runtime doesn't want to dictate choices that might be seriously inefficient for some platforms.

You could ask for C# and the runtime to possibly introduce a new set of stricter FP operations that were explicit about how things like widening/narrowing work. However, such a set of strict operations could still be built using C.

The runtime used by the compiler (at compile time) is not guaranteed to be the same at runtime.

I'm not sure how moving to C# would help this either. You still would not have this guarantee.

For example, if you read the CIL: https://www.ecma-international.org/publications/standards/Ecma-335.htm

I.12.1.3: Handling of floating-point data types

Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point type. In each such instance, the nominal type of the variable or expression is either float32or float64, but its value can be represented internally with additional range and/or precision. The size of the internal floating-point representation is implementation-dependent, can vary, and shall have precision at least as great as that of the variable or expression being represented. An implicit widening conversion to the internal representation from float32 or float64 is performed when those types are loaded from storage.

Being C or not doesn't really matter. What matters is the leeway the specification gives impls as to how these sorts of operations can perform.

So, on x64 platforms (for example), 64bit 'doubles' will often be represented in-flight in an extended precision 80-bit format. When those values are coerced back to a double location, they'll be truncated again. But in flight they may have diverged from teh values you might have gotten on another platform.

For example, if you read the CIL: https://www.ecma-international.org/publications/standards/Ecma-335.htm

I.12.1.3: Handling of floating-point data types

Storage locations for floating-point numbers (statics, array elements, and fields of classes) are of fixed size. The supported storage sizes are float32 and float64. Everywhere else (on the evaluation stack, as arguments, as return types, and as local variables) floating-point numbers are represented using an internal floating-point type. In each such instance, the nominal type of the variable or expression is either float32or float64, but its value can be represented internally with additional range and/or precision. The size of the internal floating-point representation is implementation-dependent, can vary, and shall have precision at least as great as that of the variable or expression being represented. An implicit widening conversion to the internal representation from float32 or float64 is performed when those types are loaded from storage.

Being C or not doesn't really matter. What matters is the leeway the specification gives impls as to how these sorts of operations can perform.

@CyrusNajmabadi - Well it's not so much that the current math routines are written in C but that the C runtime at compile time (used by the compiler) may not be the same as that present at runtime and this could lead to differences.

For example if it ever became possible to initialize constants by calling pure methods (e.g. Math.Pow) then there is a risk that the values computed during compilation might differ from the values computed at runtime for the same valued arguments.

If the math libraries were written in C# then the compiler could use the relevant C# math library for the target platform specified for the project being compiled.

This would guarantee that the above scenario can't arise.

For example if it ever became possible to initialize constants by calling pure methods (e.g. Math.Pow) then there is a risk that the values computed during compilation might differ from the values computed at runtime for the same valued arguments.

This would still be an issue for C# though. Because there's no pure def for FP at the CIL level.

If the math libraries were written in C# then the compiler could use the relevant C# math library for the target platform specified for the project being compiled.

Even if they were written in C, the compiler could still use them. THe primary problem is that for something to be pure you need to guarantee that it actually can be pure. This means at the specification level (regardless of impl) you need to limit to things you know will always produce the same results.

--

I get what you're trying to solve. I'm just stating that this isn't really an issue with thins being C or C#. It's an issue with the specification allowing for differing behavior, and you wanting that to combine with a feature that is about knowing strictly what the behavior will be and having that be consistent everywhere. You need to solve the latter bit first.

We don't call C runtime directly for functions where the behavior can be different on Unix. We compensate for the differences in PAL. See e.g. the PAL_pow here (pal.h defines pow as PAL_pow):
https://github.com/dotnet/coreclr/blob/8f505558e6eb3019fbe47e24898f152afa1ef790/src/pal/src/cruntime/math.cpp#L472-L557

We compensate for the differences in PAL

That isn't quite accurate. We certainly compensate for some well-defined cases in the PAL (generally cases involving NaN or Positive/NegativeInfinity), but we don't compensate for everything, and can't reasonably do so without taking the exact existing sources or significantly dropping perf for existing users on Windows.

Namely, the COMDouble and COMSingle functions have traditionally opted into fp:fast on Windows, which means that you may not always get an IEEE compliant result, but that the code in question will be faster. Turning this off should (outside any bugs in the implementation) return the IEEE compliant result, but it would significantly regress perf in doing so.

OSX and Linux each have their own implementations of the math functions as well, but we are not opting into an equivalent fp:fast switch for them (and glibc doesn't currently provide fast but non-compliant versions anyways, but they do have an issue tracking it).

Due to the complexities in the underlying algorithms and the existing implementations either being closed source, under a non-compatible OSS license, or being significantly slower than the existing implementations, it is also non-trivial to fix this problem.

So, on x64 platforms (for example), 64bit 'doubles' will often be represented in-flight in an extended precision 80-bit format. When those values are coerced back to a double location, they'll be truncated again. But in flight they may have diverged from teh values you might have gotten on another platform.

This is also not quite accurate. This was certainly true for most 32-bit x86 machines since the SSE/SSE2 instruction sets didn't exist yet when the ABIs were defined. At the time you only had the x87 FPU stack available to support IEEE floating-point in hardware. The x87 FPU internally tracks values as 80-bits but has control of the target-precision so you can compute it to the IEEE compliant 32-bit, 64-bit, or 80-bit result as required.

For 64-bit x86 machines, SSE/SSE2 were available for the first CPUs and so the ABIs (both Windows and System V, which is used by Unix systems) are dependent on those instruction sets existing. For these, the instruction set can directly operate on 32-bit or 64-bit values as needed and given that they are significantly faster than both software and the x87 FPU stack, there is no reason to not use them. -- RyuJIT was also updated to require SSE/SSE2 for 32-bit x86, so outside the ABI requirements for passing values between methods, there is no reason to use the x87 FPU stack anymore for either platform.

Additionally, since the ECMA-335 spec was last updated, there was a proof created (Innocuous Double Rounding of Basic Arithmetic Operations: https://jfr.unibo.it/article/view/4359/4017) which shows that primitive operations computed to at least double the required precision before rounding to the appropriate precision are not subject to the double-rounding bug. This means that, if the JIT sets the x87 rounding mode to 64-bit and rounds down to 32-bit after each operation (when performing 32-bit operations), the result is still IEEE compliant and you don't incur the performance hit from switching the target precision over and over (instead, you just incur the cost of an additional rounding operation which is significantly cheaper). -- Computing to 80-bits and rounding down to 64-bits can produce double-rounding errors, as can doing two operations at 64-bit precision before rounding down to 32-bits.

Realistically, given that most modern machines directly support IEEE compliant operations, the fact that the spec does define float32/float64 to be IEEE compliant everywhere else except for the blurb about storage/internal representation, and that most people would expect determinism, I see no reason why the spec couldn't be clarified to enforce this (for primitive operations) going forward. -- IEEE 754:2019 is also scheduled to be released this year, and I believe was fully approved on the 13th of June, while ECMA-335 is still referencing IEC 60559:1989 (which is duplicate to IEEE 754:1985), so updating the spec to be inline with those changes might be nice 馃槃

Was this page helpful?
0 / 5 - 0 ratings

Related issues

EgorBo picture EgorBo  路  3Comments

aggieben picture aggieben  路  3Comments

iCodeWebApps picture iCodeWebApps  路  3Comments

omajid picture omajid  路  3Comments

chunseoklee picture chunseoklee  路  3Comments