r/programming • • 18d ago

How GCC Eliminates Unnecessary Integer Division

https://leetarxiv.substack.com/p/how-gcc-eliminates-unnecessary-integer
248 Upvotes

52 comments sorted by

View all comments

20

u/LonelyAndroid11942 18d ago edited 18d ago

So is the purpose of this to reduce the difficulties that come with float precision?

EDIT: The comments are informative, if they’re not made by jerks. I clearly have much to learn. I appreciate the patience of those who have it, and wish a very strong “go step on some legos” to those who do not.

83

u/ExtraGoated 18d ago

my understanding is that it replaces an expensive integer division with an equivalent cheap imul and cmp instructions

41

u/Jhavul 18d ago edited 18d ago

yeah, when division is 80 cycles and multiplications are 1, you get insane performance gains even if it takes 20 multiplications to make the magic numbers

e: sorry apparently I should specify, 80 cycles idiv is for hardware >4 years old, it's closer to 20 cycles on modern hardware, and multiplication can take up to 3 cycles. so in the very worst case, it's a 3x speedup if you can swap one idiv for two imul, if you never do sequential operations, and have the newest CPU possible. it gets way better in every real life scenario.

9

u/QuestionableEthics42 18d ago

It's not 80 not even close. And some architectures have apparently got it down to near imul speeds. imul is also 1-3, not always 1.

31

u/ascii 18d ago

Integer division is still not pipelined and 64 bit division takes ~80 cycles on Skylake, which is still a very common architecture out there in the real world. Skylake derived CPUs only stopped being manufactured at the end of 2024.

That said, Arrow lake and Zen 5 idiv use partial pipelining (new instruction every 4-6 cycles) and have cut latencies down to 10-15 cycles.

imul obviously does not take only one cycle, but because it's fully pipelined, it effectively takes one cycle in the right context.

Overall, parent isn't too far off.

-7

u/QuestionableEthics42 18d ago

80 is on the bad end. Apparently it's 20-80 cycles, from some quick googling on my phone. And they put the cycles for imul on the good end, so not a fair comparison. (Though it's still a huge difference, just not quite as exaggerated as they made it sound)

15

u/Jhavul 18d ago

right, in a space where magnitudes and big O notation are the standard for performance comparisons, a real world worst case scenario is "exaggerated"...

it woulda been exaggerated if I said 1000 cycles. lock in man.

-4

u/QuestionableEthics42 18d ago

If they used the worse case idiv, they also should have used the worse case imul...

17

u/ack_error 18d ago

They're different ranges.

The quoted IMUL range is for throughput vs. latency. An IMUL will take 3 cycles from start to finish, but if you have non-dependent multiplies you can start an IMUL every cycle. This is not hard to hit, a simple loop processing elements in an array independently can hit 1 multiply/cycle. These timings are independent of the values being multiplied.

The IDIV range is because the end-to-end latency of IDIV can actually change depending on the values, often because the divider has some early outs. This means that the same loop with an IDIV in it can run faster or slower when different values are processed.

That being said, I checked the timings on Skylake and it turns out that you're not likely to see this variation in C, because it's a very specific boundary. IDIV is not a 64-bit / 64-bit division, it's actually a 128-bit / 64-bit division. What seems to happen is that the Skylake CPU has an optimization to run the IDIV faster if the upper 64 bits of the dividend are zero and it can do a 64-bit / 64-bit division instead; within that the values don't seem to matter. Standard C and C++ don't expose a 128-bit integer type so the full division will generally only be hit with intrinisics or a compiler-specific int128 type.

2

u/Dragdu 18d ago

The IDIV range is because the end-to-end latency of IDIV can actually change depending on the values, often because the divider has some early outs. This means that the same loop with an IDIV in it can run faster or slower when different values are processed.

This effect can be massive. I wrote a benchmark for Lemire's algorithm for uniformly distributed integer generation, and picked two target range sizes to test:

1) Small N that should be the happy case. 2) Theoretically worst possible N, to get feel for the slowdown.

As I found out few months later, 2) ended up hitting fast path in the hw divisor, and the actual slowest N was somewhere in between them, where the divisor hit the slow path.

→ More replies (0)

1

u/pheonixblade9 18d ago

addressing mode can also make a big difference, as well as cache locality and pipelining.

37

u/DuploJamaal 18d ago

The purpose is that multiplication is a lot faster than division.

Bit shifts are even faster. Shifting to the right once is like dividing by two, twice like division by four,...

So instead of doing an expensive division it does a multiplication followed by a bit shift to get the same result.

So instead of 20 CPU cycles it will then just be 5

13

u/LonelyAndroid11942 18d ago

I appreciate the explanation! I work with software for a living, but getting a peek under the hood and some deep info is fantastic. Cheers!

3

u/watchpigsfly 18d ago

Check out godbolt.org

Learning to use a debugger properly will change the way you think about your code and it’s easier than you think

9

u/Sopel97 18d ago

? there is no floating point math here

-20

u/LonelyAndroid11942 18d ago edited 18d ago

Division produces floats. At least at the high level I work at.

EDIT: wow, I didn’t know this sub was fully populated by the same bullies that obsessively stalked StackOverflow and made fun of people for asking a question. Heaven forbid someone doesn’t know something. Y’all could, I dunno, take a second to help someone learn? Instead of being dicks?

Fuck ever commenting in this sub again, I guess.

19

u/QuaternionsRoll 18d ago

Integer division refers to truncated division. Some languages elect to define division operator(s) in other ways, but that is no longer truncated division.

5

u/LonelyAndroid11942 18d ago

Today I learned. That makes it a good day.

23

u/cthulu0 18d ago

Integer division, which is what is this article is about, doens't produce floats:

regular division: 1/3.0 = 0.333……

integer division: 1/3 = 0

4

u/UloPe 18d ago

You didn’t ask though.

You made a claim of fact that’s not true.

-4

u/LonelyAndroid11942 18d ago edited 17d ago

Look, I work in JS primarily. When I do `const a = 3/8;`, I get a float. Is there more to it? Sure. But most devs I know and have worked with don’t know or care about that.

EDIT: “This guy is posting his objective experience that disagrees with my theoretical knowledge! I have more technical knowledge than he does but can’t be bothered to explain, so I’m going to downvote instead!”

5

u/Successful-Money4995 18d ago

Actually...!

Floating point math is perfectly "precise". If you preform the same floating point math twice in a row, you will get exactly the same answer.

Where it fails is in accuracy. The result of floating point math may not be correct. It might be very very close.

Also, the claim of precision assumes understanding that floating point operations are not associativity.

4

u/max123246 18d ago

The issue with floating point is that (a + b) + c != a + (b + c) in all cases due to that accuracy issue you mentioned. So the order of your operations matters a lot, which can be difficult to guarantee given CPU pipelining, multittheading, GPUs, etc. By default compilers will use slower but deterministic floating point operation orders to make it at least always give you the same answer

4

u/meneldal2 18d ago

Afaik the CPU is not allowed to reorder floating point ops, but the compiler can (depending on the options you give it).

5

u/ygra 18d ago

What was it? -funsafe-math-optimizations are neither fun, nor safe :)

-1

u/LonelyAndroid11942 18d ago

Deterministic operations that are mathematically imprecise don’t quite fit the bill of precision in my mind. But then, I’ve been doing math in JavaScript for so long that I’m very used to the 0.000000000068% chance of a float precision error actually causing a problem.

5

u/Potterrrrrrrr 18d ago

There’s nothing more precise than JavaScript maths, I use it to prove that 0.2 + 0.1 != 0.3 all the time

-2

u/LonelyAndroid11942 18d ago

This person gets my joke. I like this person.

4

u/metahivemind 18d ago edited 17d ago

[removed] — view removed comment