Transforming the CLR to numerical applications, and the nullspace that results.

Monday, January 21, 2008

Swapping byte encoding order of multi-byte numeric types in .Net (C# and VB.Net) to convert from little-endian to big-endian and back

I was looking to replace the mechanism of the swapByteOrder(Double) method in GeoAPI, since it used the BitConverter.GetBytes()/Array.Reverse()/BitConverter.ToDouble() approach. I had a feeling that this was horribly slow, given that the Array.Reverse() made me take a GC hit in a tight loop (which is how this function is often used).

Having just come off of reworking some NTS code which manipulated Double values using the bits in the number, I had gotten a much better feel of the IEEE 754 double-precision 64 bit layout. I could use BitConverter.DoubleToInt64Bits() without going pale. So, my first step was to do this - convert the Double to an Int64. I was going to use the approach I employed swapping the bytes of an Int32 (bit-shifting and or'ing) to handle Int64 values, when I had the idea that I'd check if anyone had a better idea of reversing the bytes of an Int64.

So, I was Live Searching1 for various methods of doing byte-order reversal in multi-byte numeric types, and came across this post: http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=2032515&SiteID=1. It surprised me that people were using pointers and Array.Reverse() over bit-shifting. Someone noted that the pointer version would be more highly performing, which was likely quite true, but that forces you to add the /unsafe switch to the C# compiler, which turns off verification checking and throws partial trust out the window.

So, I decided to test it.

The Methods

I found 4 methods to swap bytes in pure managed code, and created one function per method which, when given an Int64, returns that Int64 with the bytes order reversed. These might not be the best methods for all data, but it is a good sampling of what is out there.

The 4 methods and corresponding functions are:










MethodFunction
Bit ShiftingC#:
private static Int64 swapBitShift(Int64 value)
{
UInt64 uvalue = (UInt64)value;
UInt64 swapped = ((0x00000000000000FF) & (uvalue >> 56)
(0x000000000000FF00) & (uvalue >> 40)
(0x0000000000FF0000) & (uvalue >> 24)
(0x00000000FF000000) & (uvalue >> 8)
(0x000000FF00000000) & (uvalue << 8)
(0x0000FF0000000000) & (uvalue << 24)
(0x00FF000000000000) & (uvalue << 40)
(0xFF00000000000000) & (uvalue << 56));
return (Int64)swapped;
}
VB:
Function swapBitShift(ByVal value As Int64) As Int64
Dim uvalue As UInt64 = CULng(value)
Dim swapped As UInt64 = ((&HFF) And (uvalue >> 56) _
Or (&HFF00) And (uvalue >> 40) _
Or (&HFF0000) And (uvalue >> 24) _
Or (&HFF000000) And (uvalue >> 8) _
Or (&HFF00000000) And (uvalue << 8) _
Or (&HFF0000000000) And (uvalue << 24) _
Or (&HFF000000000000) And (uvalue << 40) _
Or (&HFF00000000000000) And (uvalue << 56))
Return CLng(swapped)
End Function
Pointer Byte Swapping
private static Int64 swapPointerLoop(Int64 value)
{
unsafe
{
Int64 newValue = 0;
Byte* from = (byte*)&value;
Byte* to = (byte*)&newValue;

Int32 len = sizeof(Int64);

for (int i = 0; i < len; i++)
{
to[len - i] = from[i];
}

return newValue;
}
}
IPAddress.

HostToNetworkOrder

C#:
private static Int64 swapIpAddress(Int64 value)
{
return System.Net.IPAddress.HostToNetworkOrder(value);
}
VB:
Function swapIpAddress(ByVal value As Int64) As Int64
Return System.Net.IPAddress.HostToNetworkOrder(value)
End Function
Array.ReverseC#:
private static Int64 swapArrayReverse(Int64 value)
{
Byte[] bytes = BitConverter.GetBytes(value);
Array.Reverse(bytes);
return BitConverter.ToInt64(bytes, 0);
}
VB:
Function swapArrayReverse(ByVal value As Int64) As Int64
Dim bytes As Byte() = BitConverter.GetBytes(value)
Array.Reverse(bytes)
Return BitConverter.ToInt64(bytes, 0)
End Function

I ran each test 20 times and averaged the data. I was using the test computer throughout which simulates a system experiencing taxed resources2.


The Data












1,000 Int64s graphed on a linear scale 1,000 Int64s graphed on a logarithmic scale
10,000 Int64s graphed on a linear scale 10,000 Int64s graphed on a logarithmic scale
100,000 Int64s graphed on a linear scale 100,000 Int64s graphed on a logarithmic scale

Some Conclusions


It looks like bit shifting wins out, although it's probably negligibly more efficient than IPAddress.HostToNetworkOrder. The performance of this method surprised me - not that I thought that it was implemented less efficiently, but that the method call would cost more than it does. I was further surprised to see that IPAddress.HostToNetworkOrder(Int64) makes 6 method calls per invocation, in order to delegate flipping the bytes to IPAddress.HostToNetworkOrder(Int32) twice which further delegates to IPAddress.HostToNetworkOrder(Int16) twice each. I'm not sure how much of this the JIT compiler inlines, as there is some difference between the x86 code which it generates for IPAddress.HostToNetworkOrder and the bit shifting implementation - but whatever the difference, it isn't much.


Another surprise was the pointer method. It was much slower than I thought. Perhaps it was the implementation (which, granted, is naive when we know we are looking at arrays of bytes). Perhaps it was some overhead which the C# compiler puts in when running with the usual debug settings of Visual Studio. Perhaps it was the permission asserts which the security portion of the runtime has to put on the stack when running unsafe code. If anything, since I've considered using unsafe code to speed up numeric operations, this exercise was helpful in showing me that there are some pitfalls in using it which should be investigated beforehand.


Finally, the Array.Reverse method was much worse in terms of performance that I thought it was going to be. It was a full order of magnitude worse. With the two best methods - bit shifting and IPAddress.HostToNetworkOrder so accessible, I can think of no reason to continue to use this method.


1) Yes, I'm giving it another shot, although so far, it's... not bad, but not great.


2) Ok, it doesn't simulate it exactly, but I didn't want to stop using the computer. The biggest impact will be memory usage, and make Array.Reverse() look worse than if I hadn't been using it. This is probably closer to what we want to see, since memory usage from other processes on a machine is rarely zero.

6 comments:

kidjan said...

Looking at HostToNetworkOrder in reflector, it makes sense why it has similar performance to your bit-shifting implementation:

public static long HostToNetworkOrder(long host)
{
return (long) (((HostToNetworkOrder((int) host) & 0xffffffffL) << 0x20) | (HostToNetworkOrder((int) (host >> 0x20)) & 0xffffffffL));
}

public static int HostToNetworkOrder(int host)
{
return (((HostToNetworkOrder((short) host) & 0xffff) << 0x10) | (HostToNetworkOrder((short) (host >> 0x10)) & 0xffff));
}


public static short HostToNetworkOrder(short host)
{
return (short) (((host & 0xff) << 8) | ((host >> 8) & 0xff));
}

...basically, it's shifting bits around in a very similar manner. Theirs is probably a bit more elegant, but this probably comes with a bit more overhead than the single method with eight shifts.

Rory said...

Yea, HostToNetworkOrder basically does the same thing, and the recursive definition has an elegance to it.

I was surprised that the CIL "call" instructions are so lightweight - there are 6 "call" instructions for each conversion, and the times are basically identical until we get into very large amounts of data.

The thing that bothered me more, however, is using the IPAddress class for code that has nothing to do with the System.Net namespace otherwise. Also, since the CLR is expanding to other platforms (Silverlight, .Net Micro), this method might not be available.

Finally, I'm mostly trying to show that the Array.Reverse method, which appears to be the most common out there in the wild, should be replaced without reservation.

Unknown said...

This can also be done using MemoryStream - wondered if you had tried that?

Rory said...

@william:

I looked at the performance of various memory access techniques on my other blog.

The short of it: MemoryStream holds up well when moving memory around. If you are wondering whether it would be a good place to store values as you do an endian encoding conversion on them, I'd say probably yes. If you are wondering whether creating a new MemoryStream just to swap the bytes of a multi-byte value, I'd say definately not - it would likely be slower than Array.Reverse(), but I'd like to see the data (hint, hint, nudge, nudge).

Anonymous said...

It looks like the IPAddress.HostToNetworkOrder and IPAddress.NetworkToHostOrder methods have wrong names. If the host system is big-endian (e.g. Xbox 360) both methods should not be shifting bits. If i'm not missing something they should have been named something like SwapByteOrder.

Anonymous said...

Have you tried this?

//use this function to swap bytes of any type.
#define byteSwap5(x) byteSwap((unsigned char *) &x,sizeof(x))

System::Void myClass::byteSwap(unsigned char * b, int n)
{
register int i = 0;
register int j = n-1;

//Just in case our code is ported somewhere else...
//Since we're using windows, this should always
//evaluate true.
if (!isBigEndian())
{
while (i < j)
{
std::swap(b[i], b[j]);
i++, j--;
}
}
}//end function byteSwap

System::Boolean myClass::isBigEndian()
{
//returns the endian-ness of the host machine
short word = 0x4321;
if((*(char *)& word) != 0x21 )
return true;
else
return false;
}//end function isBigEndian