in reply to Perl XS portable uint32_t

If there is not a solid 32bit type then I was contemplating a macro that is a noop on 32 bit systems, or does & 0xfffffff on 64 bit systems.

I think simply doing & 0xffffffff on 64-bit systems (whenever a value larger than 0xffffffff could be the result of an individual operation) is not such a bad idea... in case unsigned long happens to be wider than 32-bit (which can be determined easily, either at build- or at runtime). At least I would guess it's less headaches getting it to work portably, than trying to make provisions to always use the appropriate int type, where the compiler is making sure that exactly 32-bit are being used.

The additional and-operations (53 of which are needed here) are typically very fast, so the associated performance penalty on 64-bit platforms is likely going to be acceptable.  In fact, I just benchmarked it. With the test routines computing the jhash for 100 random strings of length 100000, I get on average:

Rate u32 u64 u32 54.9/s -- -13% u64 63.3/s 15% --

where 'u32' is the version with the additional & 0xffffffff instructions, producing correct results, while 'u64' is the original version producing incorrect results with 64-bit ints.

(Perl v5.8.8 built for x86_64-linux-thread-multi; gcc 4.1.0)

With a string length of only 100 chars, the difference reduces to about 4%, because the function calling overhead becomes larger, relatively... The ~14% is close to the 'true' slow down attributable to the additional and-operations — in other words, it's roughly the asymptotic limit, no longer increasing significantly with greater string lengths.

#!/usr/bin/perl use strict; use warnings; use Digest::JHash; use Benchmark qw(cmpthese); my @dat; for my $i (1..100) { $dat[$i] = ''; $dat[$i] .= chr(int(rand(256))) for 1..100000; } cmpthese (100, { u32 => sub { for (1..100) { my $h = Digest::JHash::jhash($dat[$_]); } }, u64 => sub { for (1..100) { my $h = Digest::JHash::jhash_orig($dat[$_]); } }, } );
#define MASK32 0xffffffff #define MIX(a,b,c) \ { \ a -= b; a &= MASK32; a -= c; a &= MASK32; a ^= ((c>>13)); \ b -= c; b &= MASK32; b -= a; b &= MASK32; b ^= ((a<<8) & MASK32); \ c -= a; c &= MASK32; c -= b; c &= MASK32; c ^= ((b>>13)); \ a -= b; a &= MASK32; a -= c; a &= MASK32; a ^= ((c>>12)); \ b -= c; b &= MASK32; b -= a; b &= MASK32; b ^= ((a<<16) & MASK32); \ c -= a; c &= MASK32; c -= b; c &= MASK32; c ^= ((b>>5) ); \ a -= b; a &= MASK32; a -= c; a &= MASK32; a ^= ((c>>3) ); \ b -= c; b &= MASK32; b -= a; b &= MASK32; b ^= ((a<<10) & MASK32); \ c -= a; c &= MASK32; c -= b; c &= MASK32; c ^= ((b>>15)); \ } unsigned long jhash( SV* str ) { STRLEN rawlen; char* p; unsigned long a, b, c, len, length; /* extract the string data and string length from the perl scalar +*/ p = (char*)SvPV(str, rawlen); length = len = (unsigned long)rawlen; /* Test for undef or null string case and return 0 */ if ( length == 0 ) { DEBUG && printf( "Recieved a null or undef string!\n" ); return 0; } DEBUG && printf( "Received string '%.*s'.\n", (int)len, p ); a = b = 0x9e3779b9; /* golden ratio suggested by Jenkins */ c = 0; while (len >= 12) { a += (p[0] + ((((unsigned long)p[1]) << 8) & MASK32 ) + ((((unsigned long)p[2]) << 16) & MASK32 ) + ((((unsigned long)p[3]) << 24) & MASK32 )); a &= +MASK32; b += (p[4] + ((((unsigned long)p[5]) << 8) & MASK32 ) + ((((unsigned long)p[6]) << 16) & MASK32 ) + ((((unsigned long)p[7]) << 24) & MASK32 )); b &= +MASK32; c += (p[8] + ((((unsigned long)p[9]) << 8) & MASK32 ) + ((((unsigned long)p[10])<< 16) & MASK32 ) + ((((unsigned long)p[11])<< 24) & MASK32 )); c &= +MASK32; MIX(a, b, c); p += 12; len -= 12; } c += length; switch(len) { case 11: c+=(((unsigned int)p[10]<< 24) & MASK32 ); c &= MASK32; case 10: c+=(((unsigned int)p[9] << 16) & MASK32 ); c &= MASK32; case 9: c+=(((unsigned int)p[8] << 8) & MASK32 ); c &= MASK32; case 8: b+=(((unsigned int)p[7] << 24) & MASK32 ); b &= MASK32; case 7: b+=(((unsigned int)p[6] << 16) & MASK32 ); b &= MASK32; case 6: b+=(((unsigned int)p[5] << 8) & MASK32 ); b &= MASK32; case 5: b+= ((unsigned int)p[4]); b &= MASK32; case 4: a+=(((unsigned int)p[3] << 24) & MASK32 ); a &= MASK32; case 3: a+=(((unsigned int)p[2] << 16) & MASK32 ); a &= MASK32; case 2: a+=(((unsigned int)p[1] << 8) & MASK32 ); a &= MASK32; case 1: a+= ((unsigned int)p[0]); a &= MASK32; } MIX(a, b, c); DEBUG && printf( "Hash value is %d.\n", (int)(c) ); return(c); }

Replies are listed 'Best First'.
Re^2: Perl XS portable uint32_t
by tachyon-II (Chaplain) on Jun 07, 2008 at 08:01 UTC

    Hello again almut. I would rather not just compile in the & MASK32 if it is not needed. It is easy enough to set a define in the Makefile.PL if use64bitint is set:

    DEFINE => ($Config{use64bitint} ? '-DUSING_64_BIT_INT' : ''),

    Then in the C you can use this to provide 2 versions of the MIX macro. I may well be wrong (can't test) but it seems to me that none of the & MASK32 are required in the main jhash code, provided you use a macro setup as shown below. The a b c ints will probably overflow into the low bits of the 64 bit space but because all the operations are addition the low order bits will be the valid representation in 32 bit space. Could you give this a try:

    /* Need to constrain U32 to only 32 bits on 64 bit systems * For efficiency we only use the & 0xffffffff if required */ #define USING_64_BIT_INT /* save messing with Makefile.PL define */ #if defined(USING_64_BIT_INT) #define MIX(a,b,c) \ { \ a &= 0xffffffff; b &= 0xffffffff; c &= 0xffffffff; \ a -= b; a -= c; a ^= (c>>13); a &= 0xffffffff; \ b -= c; b -= a; b ^= (a<<8); b &= 0xffffffff; \ c -= a; c -= b; c ^= (b>>13); c &= 0xffffffff; \ a -= b; a -= c; a ^= (c>>12); a &= 0xffffffff; \ b -= c; b -= a; b ^= (a<<16); b &= 0xffffffff; \ c -= a; c -= b; c ^= (b>>5); c &= 0xffffffff; \ a -= b; a -= c; a ^= (c>>3); a &= 0xffffffff; \ b -= c; b -= a; b ^= (a<<10); b &= 0xffffffff; \ c -= a; c -= b; c ^= (b>>15); c &= 0xffffffff; \ } #else #define MIX(a,b,c) \ { \ a -= b; a -= c; a ^= (c>>13); \ b -= c; b -= a; b ^= (a<<8); \ c -= a; c -= b; c ^= (b>>13); \ a -= b; a -= c; a ^= (c>>12); \ b -= c; b -= a; b ^= (a<<16); \ c -= a; c -= b; c ^= (b>>5); \ a -= b; a -= c; a ^= (c>>3); \ b -= c; b -= a; b ^= (a<<10); \ c -= a; c -= b; c ^= (b>>15); \ } #endif /* rest of code unchanged with no masking */
      I would rather not just compile in the & MASK32 if it is not needed.

      That's not what I was trying to say :)   Rather, I meant to imply two things: (a) it might be easier to just do the 32-bit masking yourself, in case you can't easily figure out what the respective magic non-standard __I32_ulong__t incantation is to get the proper 32-bit type on that yet unknown weird 64-bit platform/compiler combo, (b) the performance penalty of doing so is not as big as one might expect.

      But you're right, the amount of masking statements actually required may be simplified (I must admit, I hadn't put too much thought into the details here). In particular, masking a, b, c centrally at the beginning of the macro, saves you from having to do so at the end of various other code paths that you do come along...  Interestingly though, there's not much gain in performance from the reduced masking — I now get on average:

      Rate u32 u64 u32 56.2/s -- -12% u64 63.7/s 13% --

      (under otherwise identical conditions)

      Anyhow, I tested your simplified code suggestion, and it's working fine (at least on x86_64-linux).

      Thanks for adding another useful module to CPAN!

        Thanks for testing that. I uploaded version 0.05 a couple of days ago that implemented the dual macro, but did not set the define(s) in the Makefile.PL (I forgot!). The defines used are from Digest::MD5 which have quite a complex makefile setup. Anyway I also changed from using long to perl's U32. Now although the macro must still be the old simple macro it seems to pass on the 64 bit systems it was failing on before (4 of them).

        To test the theory that although perl warns you that you can't depend on U32 being exactly 32 bits wide I uploaded Test::U32 - so far it has not detected a failure case. I will be interesting to know.

      It is easy enough to set a define in the Makefile.PL if use64bitint is set

      Just be a little cautious with $Config{use64bitint}. It doesn't always tell you what you want/need to know. On 32-bit systems where perl is built with -Duse64bitint, the 'long' and 'int' sizes can (and generally do, I believe) remain at 4 bytes.

      I think I've also seen perls built with -Dusemorebits (the equivalent of building with -Duse64bitint && -Duselongdouble) that have neither use64bitint nor uselongdouble defined.

      And finally, it would be possible to have 64-bit longs and ints in play without having built with use64bitint support (ie when 64 bits is the size of the long/int on the particular compiler being used).

      There are probably other aspects to consider as well. (See the INSTALL file that ships with the perl source for a more authoritative account.)

      Cheers,
      Rob