Live data from Hacker News

Bitwise Division

h14s.p5r.org

61–70 of 75 posts

Re: Bitwise Division

#61
post #7

Earlier quoted context omitted.

See https://godbolt.org/z/3Yqbceaza for what godbolt says clang produces. Keep in mind that this doesn't use the fact that we know that the input is between 0 to 63.

> Keep in mind that this doesn't use the fact that we know that the input is between 0 to 63. You can use __builtin_assume for this: https://godbolt.org/z/K4jKhxnTq

9*x/64 still reduces to 2 instructions

https://godbolt.org/z/6WsWqh4ah

Re: Bitwise Division

#62
post #4

The compiler is already reducing integer division by a constant into these things. Those algorithms become more important when you're dividing by a value known at runtime but which remains the same during parts of the program. That's where libdivide comes in.

libdivide tl;dr: > libdivide allows you to replace expensive integer divides with comparatively cheap multiplication and bitshifts. Compilers usually do this, but only when the divisor is known at compile time. libdivide allows you to take advantage of it at runtime. The result is that integer division can become faster - a lot faster. [...] divide SIMD vectors by runtime constants, [1] > libdivide.h is a header-only…

The obvious question this invites: if this is generally faster for unknown values, why don't compilers use this optimization directly in emitted code?

Re: Bitwise Division

#63

Earlier quoted context omitted.

Cache pressure is not really relevant, the result can be represented in 3 bits, so the entire table can fit in three 64 bit ints. It's uglier and you still need to do bitshifts, but much easier to write.

The method in the blog post uses two bit shifts and two additions. I don't really see how to beat that with this. You can fit twenty-one 3-bit entries in a 64 bit int, with one bit to spare. So naively you get: table[nlz / 21] >> (nlz % 21) Which involves a division and a modulo. And that's assuming 63 entries in total, I'm not even trying to handle fitting the 64th entry in those three leftover bits somehow, or spre…

No need to pack bits. Use whole words. The table with 64 entries is small, and if the operation is performed often, the table will always be in L1 cache. Table lookup will take 1 cycle. The method with shifts takes 4 cycles. (It can make sense only in tight loops. The gain is really negligible in absolute terms in the large scheme of things)

Timing of instructions for Intel can be found here: www.agner.org/optimize/instruction_tables.pdf

Re: Bitwise Division

#64
You can derive a relationship between division and shift-add-multiply in a cute way by noting that

   1       1        1   1 
  ---  -  ---  =   --- --- (a-b)
   b       a        b   a

Move 1/a to the right hand side

   1      1     1   1 
  ---  = --- + --- --- (a-b)
   b      a     b   a  

               [X]
Substitute 1/b marked with X for the RHS and you get

   1     1    (a-b)       1   1
  --- = --- + ----- (1 + --- --- (a-b))
   b     a     a^2        b   a

Repeat and eventually you get

      __
  1   \    -(n+1)    n
 --- = >  a     (a-b)
  b   /_  
      n=0...inf
For example with b=3 and a=2, you get

  1     1     1     1
 --- = --- - --- + --- -+ ... 
  3     2     4     16
Word of warning though, this method tend to produce nasty carry errors.

Re: Bitwise Division

#65
post #62

Earlier quoted context omitted.

libdivide tl;dr: > libdivide allows you to replace expensive integer divides with comparatively cheap multiplication and bitshifts. Compilers usually do this, but only when the divisor is known at compile time. libdivide allows you to take advantage of it at runtime. The result is that integer division can become faster - a lot faster. [...] divide SIMD vectors by runtime constants, [1] > libdivide.h is a header-only…

The obvious question this invites: if this is generally faster for unknown values, why don't compilers use this optimization directly in emitted code?

Calculating the divisor values is also expensive, this works when you do this work once and then do the efficient divides multiple times.

Re: Bitwise Division

#66

Earlier quoted context omitted.

The method in the blog post uses two bit shifts and two additions. I don't really see how to beat that with this. You can fit twenty-one 3-bit entries in a 64 bit int, with one bit to spare. So naively you get: table[nlz / 21] >> (nlz % 21) Which involves a division and a modulo. And that's assuming 63 entries in total, I'm not even trying to handle fitting the 64th entry in those three leftover bits somehow, or spre…

No need to pack bits. Use whole words. The table with 64 entries is small, and if the operation is performed often, the table will always be in L1 cache. Table lookup will take 1 cycle. The method with shifts takes 4 cycles. (It can make sense only in tight loops. The gain is really negligible in absolute terms in the large scheme of things) Timing of instructions for Intel can be found here: www.agner.org/optimize/i…

I know, but the person I replied to specifically said "it fits in three 64 bit integers" to point out you could squeeze the table into three words. I just tried to figure out how that could possibly be advantageous here (I don't think it can).

Best I can come up with is using 32 8-bit integers (so 6h bits more than 3 times 64 bitss in size) and doing thir:

    (table[nlz >> 2] >> (nlz & 3)) & 7
… which is two shifts, two ANDs and one lookup. I'm fairly certain that two shifts and two additions beat that. Well, woulo beat that if everything else in the code wasn't likely to be a much bigger bottleneck that dwarfs the impact of this

Re: Bitwise Division

#67
post #62

Earlier quoted context omitted.

libdivide tl;dr: > libdivide allows you to replace expensive integer divides with comparatively cheap multiplication and bitshifts. Compilers usually do this, but only when the divisor is known at compile time. libdivide allows you to take advantage of it at runtime. The result is that integer division can become faster - a lot faster. [...] divide SIMD vectors by runtime constants, [1] > libdivide.h is a header-only…

The obvious question this invites: if this is generally faster for unknown values, why don't compilers use this optimization directly in emitted code?

> Compilers usually do this, but only when the divisor is known at compile time

if the divisor is in a variable or otherwise 'hidden', the compiler can't deduce enought to get to it, is my guess.

Re: Bitwise Division

#68
post #62

Earlier quoted context omitted.

libdivide tl;dr: > libdivide allows you to replace expensive integer divides with comparatively cheap multiplication and bitshifts. Compilers usually do this, but only when the divisor is known at compile time. libdivide allows you to take advantage of it at runtime. The result is that integer division can become faster - a lot faster. [...] divide SIMD vectors by runtime constants, [1] > libdivide.h is a header-only…

The obvious question this invites: if this is generally faster for unknown values, why don't compilers use this optimization directly in emitted code?

They could, but it's less profitable, and cost models are, as always, a big problem. If we have:

  loop for i from 0 to n
      f(a[i]/y)
How big does n have to be before it makes sense to compute a reciprocal for y? And how frequently is it actually that big during the runtime of the code?

Re: Bitwise Division

#69
post #45
post #8

I wonder whether the author ever ran a benchmark? Alas, the commenting system on their website seems broken, so can't ask there.

Author her. I didn't run benchmarks. I'm suspicious of micro-benchmarks and I don't have a context where I can try it against realistic data. Also, I just enjoy the maths of it even if it turns out not to make a huge performance difference in practice.

Thanks for replying! It's definitely a nice write-up.

You are right that micro-benchmarks are a bit suspicious, but they are better than nothing.

Btw, have a look at https://godbolt.org/z/zMarEnYP5 to see what Clang come up with on her own.

    #include

    uint64_t div(uint64_t nlz) {
        __builtin_assume(nlz 
This uses Risc-V assembly. Just for fun. x86 is also fascinating.

I haven't analysed it in detail. But it looks like Clang doesn't seem to mind multiplication.

Re: Bitwise Division

#70

Earlier quoted context omitted.

Cache pressure is not really relevant, the result can be represented in 3 bits, so the entire table can fit in three 64 bit ints. It's uglier and you still need to do bitshifts, but much easier to write.

The method in the blog post uses two bit shifts and two additions. I don't really see how to beat that with this. You can fit twenty-one 3-bit entries in a 64 bit int, with one bit to spare. So naively you get: table[nlz / 21] >> (nlz % 21) Which involves a division and a modulo. And that's assuming 63 entries in total, I'm not even trying to handle fitting the 64th entry in those three leftover bits somehow, or spre…

> ((table[0] >> nlz) > nlz) > nlz)

> Which is five shifts and two additions, so more work plus lookup.

Something like that, perhaps with more bitmasking and less addition. There is no lookup, just three constants loaded into different registers and handled by different out of order execution units, then combined in a final addition / or-ing. So what you will "see" on the critical path is two bitshifts and three bitwise ORs/ANDs.

It's not faster, the point is that the speedup of "binary division" is probably not worth the day or so spent developing it unless in extreme cases.

Post reply on HN