3
\$\begingroup\$

I wrote this code for a multiplayer game I am developing to transmit floats over the network. It works, on the systems that I have tested it with. What I am worried about is the little-endian big-endian thing and the possibility of different float representations in general over computers. So here it goes:

private static ByteFloat bytefloat = new ByteFloat();
[StructLayout(LayoutKind.Explicit)] struct ByteFloat
{
    [FieldOffset(0)] public float v;
    [FieldOffset(0)] public byte a;
    [FieldOffset(1)] public byte b;
    [FieldOffset(2)] public byte c;
    [FieldOffset(3)] public byte d;
}
public void setFloat(float v)
{
    bytefloat.v = v;
    setByte(bytefloat.a);
    setByte(bytefloat.b);
    setByte(bytefloat.c);
    setByte(bytefloat.d);
}
public float getFloat()
{
    bytefloat.a = getByte();
    bytefloat.b = getByte();
    bytefloat.c = getByte();
    bytefloat.d = getByte();
    return bytefloat.v;
}

I don't really want advice on variable naming or coding conventions, I just want to know if there are possible flaws with this code when the methods are called on different systems. I want to know if calling setFloat on some float, and sending the resulting bytes to any other computer, then calling getFloat on those bytes will result in the exact same value that I started with.

getByte() and setByte() do exactly what they say they do, this method pertains to a class which is a byte array wrapper, used for reading and writing data. (The backing byte array is sent over the network, and on the receiving side it is wrapped again and read from.)

If this code does not work across all systems, I would like a recommendation (or a solution) on how to write it in a way that is cross-system.

If this changes anything, I am working with the Unity platform and it's networking LLAPI (Raknet).

\$\endgroup\$
1
  • \$\begingroup\$ Quick heads up that while you may not want variable naming advice, everything that makes the code better is fair game on Code Review, so you may still get those answers. \$\endgroup\$
    – Nick Udell
    Commented Nov 26, 2015 at 9:55

2 Answers 2

3
\$\begingroup\$

Endianness matters only if your application (or another one you can communicate with) may run on environments with different CPU architectures. If you write, let's say, a Windows application it will currently run only on little-endian CPUs. Moreover you're designing your own communication protocol then endianness is simply part of your specification. If someone else will write, let's say, a compatible application running on PowerPC (!!!) then she will simply adhere to your protocol...

Also note that floating point were sometimes an exception and they did follow different rules than integers. It's pretty uncommon nowadays and, anyway, it's not something you should care about (see previous paragraph).

My suggestion is then to don't care about endianness and save that few CPU cycles you may need to perform any (unneeded) conversion.


Last point more on-topic with Code Review: you should consider to drop that custom implementation that mimic C union, there already is a fast implementation that performs same task: BitConverter.GetBytes(value) (and its counterpart BitConverter.ToSingle(array, 0)).

Doing this way you may also read a bigger buffer (second parameter of ToSingle() is an offset in array) and this will greatly improve performance (over a byte by byte read or multiple array accesses, each one with its bounds checking).

EDIT: note that if performance in this case are a real (measured) issue and you can have an unsafe assembly then you may pick their implementation dropping arguments checking (in release builds):

unsafe static byte[] GetBytes(float value) {
    var bytes = new byte[4];
    fixed (byte* b = bytes)
        *((int*)b) = *(int*)&value;

    return bytes;
}

If access is synchronized then you may even reuse same buffer:

unsafe static void GetBytes(float value, byte[] bytes) {
    Debug.Assert(bytes != null);
    Debug.Assert(bytes.Length == sizeof(float));

    fixed (byte* b = bytes)
    fixed (float* v = &value)
        *((int*)b) = *(int*)v;
}

Note that if you don't really have any performance problem then I'd keep code easier and verifiable and I'd go with BitConverter. ToSingle() implementation is straightforward.

EDIT: a small performance test (just roughly indicative because it's not your true scenario) averaging 100 executions, repeating each trial Int32.MaxValue times to have a significant execution time, keep this in mind when you compare this values (because you see a big difference only because of this):

BitConverter.ToSingle(): 19 milliseconds
Conversion using union style struct: 16 milliseconds
Conversion using unsafe pointer conversion: 7 milliseconds

Code for unsafe conversion is:

static unsafe float ToSingle(byte[] data, int startIndex) {
    fixed (byte* ptr = &data[startIndex]) {
        return *((float*)(int*)ptr);
    }
}

Vice-versa (same test conditions):

BitConverter.GetBytes(): 28 milliseconds
Conversion using union style struct: 15 milliseconds
Conversion using unsafe pointer conversion: 9 milliseconds

\$\endgroup\$
9
  • \$\begingroup\$ I opened up the BitConverter methods and apparently they have some sort of endian-ness check, for example in GetBytes(double) it checks BitConverter.SwappedWordsInDouble. I also have a method for byte to double, and it has not check like that. I assume it means my code is not portable, so I am going to take your advice and use BitConverter. Thanks for the great answer! \$\endgroup\$
    – vero
    Commented Nov 27, 2015 at 2:10
  • \$\begingroup\$ Also, would this actually be faster for turning a float into bytes, even though it creates a byte array for every conversion? \$\endgroup\$
    – vero
    Commented Nov 27, 2015 at 2:41
  • \$\begingroup\$ @Rodolvertice on which version you saw that? Microsoft implementation has not any endianness check, it simply returns raw memory representation. Exception is in converting from bytes where endianness of local machine is checked. In short: it assumes data you produce and consume have endiannness of machine where you're running. Newer versions even introduced assertions to prevent some operations on big endian machines. Word swapping for double (AFAIK) were an issue only on some older ARMs. \$\endgroup\$ Commented Nov 27, 2015 at 8:16
  • \$\begingroup\$ .NET memory allocation is pretty fast (especially for small objects) and allocating an array vs allocating a value type with same size has more or less same performance impact. What you gain is one single assignment and memory access vs 4 assignments and memory accesses. Calls are optimized away by JIT compiler then you don't need to worry about that. However there is a price you pay: arguments checking, compared to useful code its overhead is even 200/300%. \$\endgroup\$ Commented Nov 27, 2015 at 8:21
  • \$\begingroup\$ If here you have a performance issue you may simply copy & paste their implementation (assuming you can use unsafe code in your assembly): conversion with pointers cast is d*** fast compared to anything else and if you can drop arguments checking it'll be even faster. Moreover if conversion is synchronized you may even reuse same byte buffer (saving memory allocation and GC pressure). \$\endgroup\$ Commented Nov 27, 2015 at 8:23
1
\$\begingroup\$

That should not be a problem for you, since your application is writing and reading the data itself. When communication with other application, then byte order has to be taken serious.

Normally network order is big-endian, while Intel x86 machines have little-endian, so you have to convert it. You can use, for example, these methods from System.Net:

IPAddress.NetworkToHostOrder 
IPAddress.HostToNetworkOrder
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.