HiveBrain v1.2.0
Get Started
← Back to all entries
patterncsharpMinor

Float to Byte Array Serialization Over Network

Submitted by: @import:stackexchange-codereview··
0
Viewed 0 times
floatbytearrayserializationnetworkover

Problem

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).

Solution

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

Code Snippets

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

    return bytes;
}
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;
}
static unsafe float ToSingle(byte[] data, int startIndex) {
    fixed (byte* ptr = &data[startIndex]) {
        return *((float*)(int*)ptr);
    }
}

Context

StackExchange Code Review Q#111866, answer score: 3

Revisions (0)

No revisions yet.