Repository navigation
lib/gis: clarify intentional overflow in 48-bit LCG arithmetic - #8013
Conversation
Use an explicit 48-bit modular multiplication helper for the LCG jump-ahead calculations. This preserves the intended modulo 2^48 behaviour while avoiding overflowing intermediate uint64_t products reported by Coverity. Addresses CID 1687667 introduced by bb6a75c
|
Opus 5.5 had nothing to say here |
|
Model battle: Fable says this is could be AI generated detailsThe intermediate product does exceed 2^64 and gets reduced modulo 2^64 by the hardware. That is what Coverity calls an overflow. This reduction cannot change the result, for two independent reasons:
The magnitude confirms wrap actually happens: the multiplier has 35 bits and the state 48 bits, so products reach 83 bits, and in lcg_jump two 48-bit values multiply to 96 bits. None of that matters to the low 48 bits. Empirically, the review agent compared the wrapped product against an exact 128-bit product on 20 million random pairs, and I compared it against the 24-bit split on 50 million pairs plus edge cases such as all-ones and the largest power of two. All agreed. This is also the standard implementation of lrand48 in C libraries, and the comment that #7979 added and the PR deletes states exactly this argument. So the PR fixes a scanner's heuristic, not a defect. The only things that would make the wrap a real problem are a signed operand or a modulus that does not divide 2^64, and neither is present. |
This reverts commit f801e7f.
This reverts commit 94f03fe.
Add `mul48()` to encapsulate multiplication modulo 2^48 and use it in `lcg_step()` and `lcg_jump()`. The multiplication may overflow `uint64_t` and wrap modulo 2^64. This is intentional and does not affect the result modulo 2^48, since 2^48 divides 2^64. Consequently, masking the wrapped product to 48 bits gives the same result as computing the full product modulo 2^48. Centralising the operation in `mul48()` makes this property explicit and avoids repeating potentially misleading unsigned multiplication throughout the LCG implementation. It also provides a single location for documenting or suppressing static-analysis warnings about the intentional wraparound.
I expected you would cross-examine this, so I dared to create the PR. In light of this, I had a chat with GPT and it explained the issue so that I basically understand what the code actually does, and confirmed this is a false positive. It did however suggest to lift this critical multiplication to a lightweight function Suggested commit message: |
|
GCC and Clang inline the function for most Still, adding "coverity ignore" lines would also be a valid solution. Details (AI generated)Measured with gcc 13.3 and clang 18.1 on x86-64 by compiling
With the project's -O0 is not only a debugging case. Most CI jobs that build GRASS compile without optimization, because setting
Instruction counts at -O0 and -O2, from a standalone copy of
The clang -O2 difference in Part of the -O0 cost is a redundant mask. The branch masks the product inside
At -O0 the call costs more than the extra AND, and passing three arguments makes After this change Coverity will report the multiplication once, inside |
Added, hopefully this will remove all raised coverity issues. |
|
I looked at the runtime impact more closely. -O2 has the same instructions, so there is no difference, but with -O0, there is a measurable difference in both a synthetic test and with an existing tool. However, in other cases, we also prefer cleaner code and rely on optimization, so that will be the case here with this change too. Details (AI generated)Measured with gcc 13.3 and clang 18.1 on x86-64, pinned to one core, with Synthetic test: a loop of 10^8
Existing tool: r.sim.water from #7980, which draws through
That is +2.0 s (+4.9 %), with every run of this branch slower than every run of main. The output depth is identical. r.sim.water was not timed at -O2, where the compiled |
|
wenzeslaus
left a comment
(OSGeo/grass#8013)
<#8013 (comment)>
I looked at the runtime impact more closely. -O2 has the same instructions, so there is no difference, but with -O0, there is a measurable difference in both a synthetic test and with an existing tool. However, in other cases, we also prefer cleaner code and rely on optimization, so that will be the case here with this change too.
For anyone compiling with -O0, speed isn't the main priority , I'd say. So that shouldn't be a concern here either.
|
wenzeslaus
left a comment
There was a problem hiding this comment.
I agree that the slower -O0 case is not an issue and that in other cases, we would go with more readable code (and rely on optimization to solve it for us). We may need to follow up with using -O2 in CI.
The figures were made with the head of OSGeo#7980 before it was merged. Rerun the benchmark script on this branch merged with main, which now contains OSGeo#7980 (per-walker random numbers) and OSGeo#8013 (the 48-bit multiplication in a helper), with the same sizes, threads and repeats. The times agree with the earlier run within about 2 % (1 m: 113.1 s with 1 thread, 12.2 s with 16, speedup 9.3). Written with the help of Claude Code.
Use an explicit 48-bit modular multiplication helper for the LCG jump-ahead calculations. This preserves the intended modulo 2^48 behaviour while avoiding overflowing intermediate uint64_t products reported by Coverity.
Addresses CID 1687667 introduced by bb6a75c
Created with assistance by Codex.
Updated, see #8013 (comment)