Runtime: Support natural size data types in the CLR

Created on 8 May 2015  路  25Comments  路  Source: dotnet/runtime

There are cases (in particular on OSX) where native APIs are defined in terms of the natural size of the platform. This means that APIs use 32-bit integers on 32-bit platforms and 64-bit integers in 64-bit platforms.

Mono uses a poor man's approach to solve this problem, and we introduce the structures System.nint, System.nuint and System.nfloat that stand for native integer, native unsigned integer, and native float. These data types will be 32 bits or 64 bits depending on the platform.

The usual set of operators on ints, uints and floats are defined on those structures.

The Mono runtime then has special support to turn sequences that would invoke those operators into the native representation for it.

This means that code that would pass two nint structures, and call the addition operator on it are turned into a regular integer addition operation with no performance loss.

This is the poor man's way of introducing a new data type that is not part of the CIL spec.

This is what we published on the subject:

http://developer.xamarin.com/guides/cross-platform/macios/nativetypes/

area-Meta

Most helpful comment

This proposal needs to explain why we need more than just nicer IntPtr support at the C# level.

There are other .NET languages.

All 25 comments

This sounds like a good candidate for independent (Apple-specific?) contract and NuGet package. It should not need to be wired into CoreCLR runtime.

RyuJIT is pretty good about generating code for struct wrappers. Struct wrappers are frequently used for performance critical paths. The struct promotion optimization was specifically designed to optimize them.

I have done a quick experiment:

```c#
public struct nint
{
long _value;

public nint(long value)
{
    _value = value;
}

public static implicit operator nint(int value)
{
    return new nint(value);
}

static public nint operator+(nint a, nint b)
{
    return new nint(a._value + b._value);
}

}

...
static nint PlusOne(nint x)
{
return x + 1;
}
...

Code:

ConsoleApplication2!ConsoleApplication2.Program.PlusOne(ConsoleApplication2.nint):
00007ff9761404b0 488d4101 lea rax,[rcx+1] 00007ff9761404b4 c3 ret
```

About as good code as it can be.

If there are specific natural data type patterns that are hard to optimize in general way, we can consider treating them in the JIT as intrinsic. Same as SIMD Vector types are treated today - vector types live in independent contract and NuGet package as well. I cannot think about a good example where such treatment would be required.

How is this different from the native int type defined in ECMA-335 搂I.8.2.2 and 搂I.12.1.1, and mentioned in the operand type table in 搂III.1.5?

@sharwell agreed, nint and nuint are redundant. However nfloat would remain a problem.

One way to do this, today, would be to compile both multiple assemblies with the CPU architecture flag set (with a Single or Double in your struct depending on the arch). System.AppDomain.CurrentDomain.AssemblyResolve could then be used to load the correct one into the process when it is requested.

What would be awesome is if you could host a GAC (LAC?) alongside your bin, and not have to monkey about with AssemblyResolve.

Just to add to Jan's point, singleton structs are very common already in .NET and so we strive to make them as efficient as scalar code as we can. There are certainly cases today where we fall short but we'd take such cases as an opportunity to do better.

This is not an Apple specific issue. Anywhere P/Invoke encounters a type such as 'sizet', the API differs on 32bit vs 64bit. System.IntPtr and SystemUIntPtr can be used in these places as its 4 bytes on 32bit and 8 bytes on 64bit and has a size property for runtime 'discovery'. The issue with it is that you must cast to perform arithmetic.

struct NativeInteger
{
IntPtr value;

... explicit conversions, etc ...
}

Is probably an end-user solution to it.

With regards to floating point, ECMA 335 I.8.2.2 explicitly defines System.Single to be 32bit and System.Double to be 64bit. There is no type that changes based on platform such as IntPtr. However, I have never encountered a need for such

Isn't this primarily a semantic issue, more related to language design than a codegen issue?

native machine size types are already supported by the clr as @sharwell, just not really exposed nicely in the upper layers like c#.

The main issue is that IntPtr / UIntPtr, while containing the right size, are really not full blown types, in the sense that substracting two (U)IntPtr and comparing them is not supported for the most part...

Sure, I could write nint and nuint and implement every operator in existence to make them feel native. but this should really be part of the language, via whatever means the language designers feel are necessary.

When it actually does come to what IL to emit, there could be a discussion about structs vs. native int, but given that the native types DO exist, it would be weird not to use them...

@migueldeicaza Isn't this more of a language feature given that the CLR / MSIL already supports all that you requested for?

Shouldn't this issue be actually closed (here) and opened up in Roslyn?

The actual case where there is a 'native int' difference such as pointers on 32bit vs 64bit is covered already by System.IntPtr, System.UIntPtr. There does not really seem to be a case where there is actually a 'native float'. On Apple systems, there is a difference from the API calls on OSX versus iOS, but those are not platform specific: both OSX and iOS have System.Single and System.Double native types, however, a 32bit iOS does not have 64bit 'native integer type' where as a 64bit iOS does. Additionally, APIs (such as OpenGL) outside of the OS will consistently use System.Single or System.Double.

This means the latter case of a 'native float' is not actually a technical issue as much as it is a convenience issue. This can be solved by adding an abstracted type to the binding (managed wrapper) rather than the language. The concern seems to be overhead in the resulting JIT code. As illustrated earlier in this thread, the new JIT can deal with the abstraction.

Since its possible for the bindings to conform to the Common Language Specification (or whatever its called today), its not an issue for Roslyn since any .Net language (Visual Basic, F#, Iron Python, etc) may consume the binding.

In conclusion, if there were supported platforms where System.Single is native but not System.Double, it would be the same need as System.Int32 versus System.Int64 (solved by System.IntPtr). However, as of today, all platforms including OSX and iOS offer System.Double as a native type. That being said, this issue is probably best solved by an abstracted System.ValueType (struct) in the API binding (managed wrapper).

As it was already explained before, there are various issues with the proposals.

IntPtr/UIntPtr do not define all math operators. You could work around them by having your own version of those, and hoping that the JIT will do the right thing.

There is no equivalent for floating point.

Now, I understand the desire to not make changes on something like .NET desktop, where the pain is high. But I thought the point of .NET core is to actually address fundamental problems, and this is one of them.

@migueldeicaza My initial recommendation is that you split this request into two issues.

  1. Update IntPtr and UIntPtr to support the same set of operators the CLR supports on the underlying native int type. I think you could make a strong case for this, and I think it has a higher likelihood of being accepted. I do anticipate some arguments against this, so consider alternatives like a NativeMath class which provides these operators as separate methods implemented as JIT intrinsics - or perhaps even as directly recognized by the C# compiler.
  2. A separate issue for a native-size floating point data type. The merits of this issue as well as the target audience is likely to differ somewhat from the previous issue.

@jkotas could you elaborate on the types of structs the struct promotion optimization targets? For example, how many fields and of what type before the optimization kicks-in or turns off?

The struct has to have no more than 4 non-overlapping fields, and the total size of the struct has to be no more than 32 bytes.

The exact algorithm is in https://github.com/dotnet/coreclr/blob/3593c4f00a93054d1507a621c6b11f8b45a9c300/src/jit/lclvars.cpp#L1379

@CarolEidt is working on a first class structs support in the optimizer - that may relax some of these limitations.

With regard to "first class structs" support, the primary objectives are 1) to ensure that structs are treated as close to primitive types as possible when it comes to value numbering, CSE, constant propagation and copy propagation, and 2) to support full-enregistration of structs that fit into register(s) and whose fields are not referenced. The design for this is here: https://github.com/dotnet/coreclr/blob/master/Documentation/design-docs/first-class-structs.md
It is not an immediate objective to modify the struct promotion conditions (except to relax some of the constraints with regard to structs that are passed and returned). However, I'm open to revisiting that as well, once we've got the other work items completed.

@jkotas Would the operator+ be inlined in a R2R scenario? From the spec:

As an example, consider the issue of inlining of method bodies. If module A would inline a method from Module B, that would break our desired versioning property because now if that method in module B changes, there is code in Module A that would need to be updated (which we do not wish to do). Thus inlining is illegal across modules. Inlining within a module, however, is still perfectly fine.

So if PlusOne and operator+ are in different _versioning bubbles_ would that prevent RyuJIT from inlining? If so, will guidance be that a versioning bubble should include all libraries exposing structs whose methods should be inlined?

R2R supports [NonVersionable] attribute that allows methods that are set in stone and not going to ever change to be inlined across version boundaries. Operators on natural sized int would be a perfect candidate for it.

This attribute is used in on IntPtr/UIntPtr operators today - https://github.com/dotnet/coreclr/blob/775003a4c72f0acc37eab84628fcef541533ba4e/src/mscorlib/src/System/UIntPtr.cs#L132

BigInteger could benefit to have _sign and _bits defined as nuint (UIntPtr) in this way more numeric bits could be expressed in the array in 64 bit processors and values in the long range could be expressed using only _sign.

This proposal needs to explain why we need more than just nicer IntPtr support at the C# level. It was already stated that IntPtr at the CLR level is fully sufficient (since it is redundant to nint as explained).

@tannergooding Is this now covered by the other work taking place in C# vNext? Is there anything else that needs to be done for this issue before we close it out?

This proposal needs to explain why we need more than just nicer IntPtr support at the C# level.

There are other .NET languages.

@benaadams, could you elaborate?

Other languages (such as F#) already support IntPtr/UIntPtr with operators since it is the framework type defined to match the runtime native int type (which itself has the direct IL support). C# is just exposing the new nint/nuint types to be partially erased wrappers over IntPtr/UIntPtr so that the operators can be exposed in a non breaking way (that is, you will write nint but it will emit as [AttributeIdentifyingAsNativeInt] IntPtr in metadata: https://github.com/dotnet/csharplang/pull/2833).

nfloat is a new concept (to the runtime, framework, and language) but is realistically macOS/iOS specific. Apple has also been dropping 32-bit support entirely and there is really only legacy and a small selection of other devices (such as the watch, iirc) where this would be relevant. This leads me to believe that nfloat would be best supported by a user (or possibly framework) defined type that is float or double depending on the target platform bitness.

So VB will be able to do use apis that expose nint (as they will be IntPtr); however the api changes for it don't expose comparison operators for IntPtr https://github.com/dotnet/runtime/issues/21943; so you couldn't use it as a loop iterator value without doing i.CompareTo((IntPtr)upperbound) = 0

however the api changes for it don't expose comparison operators

Right, IntPtr is functionally the same as Int32 (they are both runtime defined primitive types) and so it shouldn't directly expose user defined operators (and it minimally doing so in .NET 4 is one reason why we needed to go down the partial erasure route). Languages will need to treat it as a primitive type and emit the right IL instructions if they want operator support to be available.

@KathleenDollard or @jaredpar might be able to comment on if there are any plans for VB in particular.

might be able to comment on if there are any plans for VB in particular.

There are not for nint

The bulk of this is covered by nint/nuint added for C# 9.

If we still need nint/nuint/nfloat for Xamarin.iOS compatibility, it would be best to create a new issue for it.

Was this page helpful?
0 / 5 - 0 ratings