Bit specific code in agnostic assemblies???

In previous blog entries I’ve spent some time talking about how to mark assemblies as bit specific and how the loader deals with those markings.

What however is the preferred mode of an application? I will posit that it is to be compiled agnostic and to run equally well on both 32-bit and 64-bit platforms. It makes a lot of things easier: development, build, testing, deployment, servicing…

Caveat: The following discussion deals only with fully IL assemblies. If you generate managed C++ code you may end up with some native code in your image at which point it has to be tied to one specific platform.

If you have a reason to tie yourself to only one platform (e.g. x86 because you only have an x86 version of some native DLL that you need to P/Invoke to) then your decision is easy and it’s been made for you. Just flip the /platform:x86 switch on your compiler and go. However, if you have some code that works on both 32-bit and 64-bit platforms with just some subtle difference then you have a couple of options to think about for implementing the differences:

1) Use compile time defines (#if/#else in C#) to separate your 64-bit code from 32-bit code. Use the /platform:X switch of your compiler to generate different assemblies for 32-bit and (both) 64-bit platforms.
2) Use runtime if/else blocks to separate 32-bit and 64-bit code.

Both of these end up having their place. In most cases I’ve seen, people have only a small amount of code which needs to be bit specific, and in those cases dealing with the rest of the hassles around building and deploying multiple assemblies aren’t really worth it…

But, what about the runtime cost of the check? What if your bit specific code is on the hot path? Won’t that hurt?

Actually, that’s the cool part, if you do it right it won’t[1]. So, what are your options for determining bitness of a process at runtime?

A) if (Marshal.SizeOf(IntPtr.GetType()) == 8) {/*64*/} else {/*32*/ }
B) if (IntPtr.Size == 8) {/*64*/} else {/*32*/}
C) readonly static bool is64Bit = (IntPtr.Size==8);
    if (is64Bit) {/*64*/} else {/*32*/}

Of those options there are 2 right ways and a wrong way. Unfortunately, some of the early information coming out of Microsoft indicated that you should use Marshal.SizeOf, which is definitely the wrong way to do this. That check involves a call to the marshaling code in mscorlib.dll and since the JIT (or ngen) compiler doesn’t know at JIT (or ngen) time what the result will be the unused half of the code can’t be optimized away as dead code.

The easiest way to do this is B, since IntPtr.Size is a constant which is hard-coded into mscorlib.dll when we build the runtime, the JIT (or ngen) can check the loaded mscorlib.dll (which will vary depending on bitness) and optimize away the check and the unused half of the code.

Option C works also, but it has a potentially subtle bug to it. If you don’t mark the static variable definition as readonly then the JIT (and ngen) won’t be able to optimize away the check and unused code. This is because it has to assume that the value can change at runtime. This is very important to remember because without this keyword this solution will become almost as bad as A.

Recommendation: for simple cases, use “if (IntPtr.Size==8)” to determine 64-bitness. For more complex cases consider using a static boolean, but remember to mark it as readonly.

Unfortunately the if/else solution won’t work well for cases where you need different structure definitions on 64-bit and 32-bit platforms for P/Invoke-ing to native routines. If you have a very small number of usages you might consider having two separate structure definitions and P/Invoke declarations, and using if/else to determine which one you use (maybe hiding the bitness stuff behind a wrapper). However, if it is a frequently used structure then it probably makes more sense to just use platform specific assemblies and compile time defines to determine structure layout as then changes only need to be made at the structure definition site.

If you’d like to see some of this stuff in action I’ve posted the source to a test that you can run and then inspect in the debugger to see what the JIT does ( I’m sure it can be done more easily in VS, but I’ve been using WinDbg with SOS’s !name2ee and !u commands to disassemble the resulting code.

[1] Well, there is a small cost involved in the JIT having to parse the extra IL code for both platforms before it can evaluate the const condition and throw away half. However this cost is minimal and for frequently executed code is trivial. For ngen’d code the cost at runtime is non-existant.

Comments (7)

  1. This is useful information. If you use PInvoke, you are then "locked" into distributing 2 assemblies, and this technique won’t work? There is no way around this?

  2. joshwil says:

    Using P/Invoke doesn’t automatically lock you into 2 assemblies. For instance, P/Invoking to the Win32 API will work find as long as the signatures are correctly defined (i.e. things that scale are defined as IntPtr/UIntPtr and the structs don’t have wierd packed layouts that can’t be easily transitioned to 64-bit).

    Additionally if you are writing your own native code to P/Invoke to as long as you provide an interface that follows these conventions (and your dlls are named the same thing on both platforms, you may have to do some path magic to make this work) then everything will just work with an agnostic assembly.

    The problems crop up when you have something like a structure that has wierd packing that doesn’t carry over to 64-bit or you have different interfaces on 32-bit and 64-bit. Even in this case as mentioned above if the functionality is the same and it is simple enough you could wrap the native call with a managed helper which knows (using the if(IntPtr.Size==8)/else trick) how to call the appropriate API depending on the platform and magically do the right thing (whatever that is) with arguments and return values.

  3. Ok, I think I understand. I did do quite a bit of work porting from 32 to 64-bit processors in the past (alpha), so I understand the issues. It sounds like PInvoking to Win32 is not a problem, as long as we use the right signature.

    So let’s consider that other case, PInvoking to our own unmanaged dlls — and specifically what if you want one unmanaged build for both 32 and 64, is that an absurd idea? In the past I would think it would not even run… Maybe you have covered this before.

    Can you give an example of a structure with weird packing? I understand that if you assume pointers are the same size everything is messed up.

  4. joshwil says:

    Frank —

    I’m not sure I understand the question. Are you asking if you can have a single unmanaged DLL and P/Invoke to it from managed code running under either the 32-bit or 64-bit runtimes?

    If that is the question then the answer is no. Given that your unmanaged code has to be either 32-bit or 64-bit and a process can only load DLLs of its native bitness then you won’t be able to load your unmanaged DLL in one of the processes.

    I’ll try to find an example of a structure that breaks on 64-bit, I don’t have one at the ready (most don’t).

  5. V2 CLR added support for 64-bit (amd64 and ia64), and that includes managed-debugging support. So a 64-bit…