Search code examples
c#luafnv

Porting FNV-1a algorithm from C# to Lua, multiplication result don't match


I'm trying to port the Accidental Noise library from C# to Lua. I encounter an issue when trying to port the FNV-1A algorithm. The result of the multiplication with the prime doesn't match when using same input values.

First I'd like to show the C# code of the algorithm:

// The "new" FNV-1A hashing
private const UInt32 FNV_32_PRIME = 0x01000193;
private const UInt32 FNV_32_INIT = 2166136261;

public static UInt32 FNV32Buffer(Int32[] uintBuffer, UInt32 len)
{
    //NOTE: Completely untested.
    var buffer = new byte[len];
    Buffer.BlockCopy(uintBuffer, 0, buffer, 0, buffer.Length);

    var hval = FNV_32_INIT;    
    for (var i = 0; i < len; i++)
    {
        hval ^= buffer[i];
        hval *= FNV_32_PRIME;
    }

    return hval;
}

This function is called as such (simplified) elsewhere in the codebase:

public static UInt32 HashCoordinates(Int32 x, Int32 y, Int32 seed)
{
    Int32[] d = { x, y, seed };
    return FNV32Buffer(d, sizeof(Int32) * 3);
}

I noticed the sizeof(Int32) result is always multiplied by the number of elements in the Int32[] array. In this case (on my machine) the result is 12, which causes the buffer size in the FNV32Buffer function to be an array of 12 bytes.

Inside the for loop we see the following:

  1. A bitwise XOR operation is performed on hval
  2. hval is multiplied by a prime number

The result of the multiply operation doesn't match with the result of my Lua implementation.

My Lua implementation is as such:

local FNV_32_PRIME = 0x01000193
local FNV_32_INIT = 0x811C9DC5

local function FNV32Buffer(buffer)
    local bytes = {}

    for _, v in ipairs(buffer) do
        local b = toBits(v, 32)
        for i = 1, 32, 8 do
            bytes[#bytes + 1] = string.sub(b, i, i + 7)
        end
    end

    local hash = FNV_32_INIT
    for i, v in ipairs(bytes) do
        hash = bit.bxor(hash, v)
        hash = hash * FNV_32_PRIME
    end

    return hash
end 

I don't supply the buffer length in my implementation as Lua's Bitwise operators always work on 32-bit signed integers.

In my implementation I create a bytes array and for each number in the buffer table I extract the bytes. When comparing the C# and Lua byte arrays I get mostly similar results:

byte # C# Lua
1 00000000 00000000
2 00000000 00000000
3 00000000 00000000
4 00000000 00000000
5 00000000 00000000
6 00000000 00000000
7 00000000 00000000
8 00000000 00000000
9 00101100 00000000
10 00000001 00000000
11 00000000 00000001
12 00000000 00101100

It seems due to endianness the byte ordering is different, but this I can change. I don't believe this has anything to do with my issue right now.

For the C# and Lua byte arrays, I loop through each byte and perform the FNV-1A algorithm on each byte.

When using the values {0, 0, 300} (x, y, seed) as input for the C# and Lua functions I get the following results after the first iteration of the FNV hashing loop is finished:

C#: 00000101_00001100_01011101_00011111 (84696351)

Lua: 01111110_10111100_11101000_10111000 (2126309560)

As can be seen the result after just the first hashing loop are very different. From debugging I can see the numbers diverge when multiplying with the prime. I believe the cause could be that Lua uses signed numbers by default, whereas the C# implementation works on unsigned integers. Or perhaps the results are different due to differences in endianness?

I did read that Lua uses unsigned integers when working with hex literals. Since FNV_32_PRIME is a hex literal, I guess it should work the same as the C# implementation, yet the end result differs.

How can I make sure the Lua implementation matches the results of the C# implementation?


Solution

  • LuaJIT supports CPU native datatypes.
    64-bit values (suffixed with LL) are used to avoid precision loss of multiplication result.

    -- LuaJIT 2.1 required
    local ffi = require'ffi'
    
    -- The "new" FNV-1A hashing
    local function FNV32Buffer(data, size_in_bytes)
       data = ffi.cast("uint8_t*", data)
       local hval = 0x811C9DC5LL
       for j = 0, size_in_bytes - 1 do
          hval = bit.bxor(hval, data[j]) * 0x01000193LL
       end
       return tonumber(bit.band(2^32-1, hval))
    end
    
    local function HashCoordinates(x, y, seed)
       local d = ffi.new("int32_t[?]", 3, x, y, seed)
       return FNV32Buffer(d, ffi.sizeof(d))
    end
    
    print(HashCoordinates(0, 0, 300))  --> 3732851086