Skip to content

Speedup collision checking by avoiding expensive sqrt()#25

Open
d0sboots wants to merge 1 commit into
ianh:masterfrom
d0sboots:speedup
Open

Speedup collision checking by avoiding expensive sqrt()#25
d0sboots wants to merge 1 commit into
ianh:masterfrom
d0sboots:speedup

Conversation

@d0sboots

@d0sboots d0sboots commented Apr 1, 2026

Copy link
Copy Markdown

Test timings show a >5% speedup for large solutions.


sqrt is a slow function, and should be avoided in performance-sensitive code. This transformation to use xy_len2 instead saves a noticable amount of time. I also cleaned up a few functions to use the float versions, and fixed a compiler warning.

Here's a comparison of test timings, filtered to those that take more than 0.1 seconds on my machine:

Before:

1.16520 C=5092 A=775 test/solution/tourney-2021/Week 6/Steven_/ravaris-rage-OM2021_W6-17.solution
0.52614 C=1981 A=11075 test/solution/tourney-2021/Week 3/winter1703/mist-of-dousing-OM2021_W3-1.solution
6.91026 C=7975 A=72887 test/solution/tourney-2021/Week 3/Mattermonkey/Mattermonkey_-_Cost.solution
0.18461 C=3451 A=2264 test/solution/tourney-2021/Week 3/biggiemac42/mist-of-dousing-OM2021_W3-35.solution
0.11618 C=249 A=2001 test/solution/tourney-2021/Week 3/Steven_/mist-of-dousing-OM2021_W3-8.solution
1.66390 C=3527 A=31530 test/solution/tourney-2021/Week 3/mr_puzzel/mist-of-dousing-OM2021_W3-2.solution
0.62635 C=8971 A=13228 test/solution/tourney-2021/Week 3/Noeuchar/mist-of-dousing-OM2021_W3-3.solution
0.17319 C=3676 A=12803 test/solution/tourney-2021/Week 3/Noeuchar/mist-of-dousing-OM2021_W3-6.solution
0.13075 C=2292 A=12407 test/solution/tourney-2021/Week 3/rolamni/rolamni_OM2021_W3_GC.solution
0.15471 C=1011 A=4347 test/solution/tourney-2021/Week 8/Steven_/elemental-jewel-setting-OM2021_W8-13.solution
0.26335 C=756 A=5785 test/solution/tourney-2021/Week 0/biggiemac42/a-welcome-to-house-colvan-OM2021_W0-6.solution
0.18773 C=897 A=1055 test/solution/tourney-2021/Week 0/biggiemac42/a-welcome-to-house-colvan-OM2021_W0-12.solution
0.24116 C=754 A=5559 test/solution/tourney-2021/Week 0/biggiemac42/a-welcome-to-house-colvan-OM2021_W0-6 (1).solution
0.50058 C=1133 A=5731 test/solution/tourney-2021/Week 0/PentaPig/a-welcome-to-house-colvan-OM2021_W0-1.solution
0.18794 C=960 A=1511 test/solution/tourney-2021/Week 0/PentaPig/a-welcome-to-house-colvan-OM2021_W0-9.solution
0.11721 C=27728 A=68 test/solution/tourney-2021/Week 0/Steven_/a-welcome-to-house-colvan-OM2021_W0-7.solution
0.22888 C=805 A=5394 test/solution/tourney-2021/Week 0/panic/40-805-5394.solution
0.43297 C=1399 A=24547 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_GI.solution
0.50363 C=204 A=13645 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_notrackI.solution
0.25739 C=754 A=5786 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_GC.solution
0.41706 C=427 A=12557 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_GC (1).solution
0.26404 C=757 A=5786 test/solution/tourney-2021/Week 0/Starficz/a-welcome-to-house-colvan-OM2021_W0-4.solution
0.10268 C=3153 A=2058 test/solution/tourney-2019/week2/GA-ShadowCluster.solution
0.59491 C=7577 A=4862 test/solution/jinyou-archive/734_Refined Bronze_B_GAX_NOGIF_60G_7577C_4862A_7577I_8P.solution
0.58280 C=7507 A=4960 test/solution/jinyou-archive/734_Refined Bronze_B_GC_NOGIF_60G_7507C_4960A_7507I_8P.solution
0.15932 C=11453 A=1789 test/solution/tourney-2020/Week 8/Mattermonkey/Mattermonkey_-_1415G_11453_1789.solution
0.10593 C=21359 A=188 test/solution/tourney-2020/Week 8/tw33dl3dee/metal-calculus-tw33dl3dee-no-metric.solution
0.10772 C=34284 A=87 test/solution/tourney-2020/Week 8/tw33dl3dee/metal-calculus-tw33dl3dee-area-87.solution
0.19762 C=140683 A=50 test/solution/tourney-2020/Week 8/PentaPig/output.solution
0.22525 C=157663 A=59 test/solution/tourney-2020/Week 8/jinyou/948_OM2020_W8_MetalCalculus_JINYOU_WASTER_9_AREA_CORRECT_OUT_225G_157663C_59A_3165I_15P (1).solution
0.21379 C=6700 A=600 test/solution/tourney-2020/Week 8/LarsDahl/metal-calculus-OM2020_W8_MetalCalculus-1.solution
0.23540 C=104991 A=108 test/solution/tourney-2020/Week 8/Bambi/metal-calculus-OM2020_W8_MetalCalculus-6.solution
0.21489 C=109276 A=54 test/solution/tourney-2020/Week 8/rolamni/rolamni_OM2020_W8_A.solution
0.15051 C=7661 A=865 test/solution/tourney-2020/Week 8/RP0/metal-calculus-OM2020_W8_MetalCalculus-3.solution
0.59112 C=138691 A=174 test/solution/tourney-2020/Week 0/jinyou/940_OM2020_W0_ConnectTheDots_JINYOU_2310_485G_138691C_174A_80I_24P.solution

After:

1.04379 C=5092 A=775 test/solution/tourney-2021/Week 6/Steven_/ravaris-rage-OM2021_W6-17.solution
0.49654 C=1981 A=11075 test/solution/tourney-2021/Week 3/winter1703/mist-of-dousing-OM2021_W3-1.solution
6.48372 C=7975 A=72887 test/solution/tourney-2021/Week 3/Mattermonkey/Mattermonkey_-_Cost.solution
0.17469 C=3451 A=2264 test/solution/tourney-2021/Week 3/biggiemac42/mist-of-dousing-OM2021_W3-35.solution
0.11154 C=249 A=2001 test/solution/tourney-2021/Week 3/Steven_/mist-of-dousing-OM2021_W3-8.solution
1.56796 C=3527 A=31530 test/solution/tourney-2021/Week 3/mr_puzzel/mist-of-dousing-OM2021_W3-2.solution
0.59224 C=8971 A=13228 test/solution/tourney-2021/Week 3/Noeuchar/mist-of-dousing-OM2021_W3-3.solution
0.16361 C=3676 A=12803 test/solution/tourney-2021/Week 3/Noeuchar/mist-of-dousing-OM2021_W3-6.solution
0.12350 C=2292 A=12407 test/solution/tourney-2021/Week 3/rolamni/rolamni_OM2021_W3_GC.solution
0.15092 C=1011 A=4347 test/solution/tourney-2021/Week 8/Steven_/elemental-jewel-setting-OM2021_W8-13.solution
0.24868 C=756 A=5785 test/solution/tourney-2021/Week 0/biggiemac42/a-welcome-to-house-colvan-OM2021_W0-6.solution
0.17780 C=897 A=1055 test/solution/tourney-2021/Week 0/biggiemac42/a-welcome-to-house-colvan-OM2021_W0-12.solution
0.22784 C=754 A=5559 test/solution/tourney-2021/Week 0/biggiemac42/a-welcome-to-house-colvan-OM2021_W0-6 (1).solution
0.47136 C=1133 A=5731 test/solution/tourney-2021/Week 0/PentaPig/a-welcome-to-house-colvan-OM2021_W0-1.solution
0.17742 C=960 A=1511 test/solution/tourney-2021/Week 0/PentaPig/a-welcome-to-house-colvan-OM2021_W0-9.solution
0.11363 C=27728 A=68 test/solution/tourney-2021/Week 0/Steven_/a-welcome-to-house-colvan-OM2021_W0-7.solution
0.21611 C=805 A=5394 test/solution/tourney-2021/Week 0/panic/40-805-5394.solution
0.40867 C=1399 A=24547 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_GI.solution
0.47582 C=204 A=13645 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_notrackI.solution
0.24256 C=754 A=5786 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_GC.solution
0.39318 C=427 A=12557 test/solution/tourney-2021/Week 0/rolamni/rolamni_OM2021_W0_GC (1).solution
0.24847 C=757 A=5786 test/solution/tourney-2021/Week 0/Starficz/a-welcome-to-house-colvan-OM2021_W0-4.solution
0.09718 C=3153 A=2058 test/solution/tourney-2019/week2/GA-ShadowCluster.solution
0.56379 C=7577 A=4862 test/solution/jinyou-archive/734_Refined Bronze_B_GAX_NOGIF_60G_7577C_4862A_7577I_8P.solution
0.55238 C=7507 A=4960 test/solution/jinyou-archive/734_Refined Bronze_B_GC_NOGIF_60G_7507C_4960A_7507I_8P.solution
0.15407 C=11453 A=1789 test/solution/tourney-2020/Week 8/Mattermonkey/Mattermonkey_-_1415G_11453_1789.solution
0.10437 C=21359 A=188 test/solution/tourney-2020/Week 8/tw33dl3dee/metal-calculus-tw33dl3dee-no-metric.solution
0.10543 C=34284 A=87 test/solution/tourney-2020/Week 8/tw33dl3dee/metal-calculus-tw33dl3dee-area-87.solution
0.19539 C=140683 A=50 test/solution/tourney-2020/Week 8/PentaPig/output.solution
0.22203 C=157663 A=59 test/solution/tourney-2020/Week 8/jinyou/948_OM2020_W8_MetalCalculus_JINYOU_WASTER_9_AREA_CORRECT_OUT_225G_157663C_59A_3165I_15P (1).solution
0.20676 C=6700 A=600 test/solution/tourney-2020/Week 8/LarsDahl/metal-calculus-OM2020_W8_MetalCalculus-1.solution
0.23298 C=104991 A=108 test/solution/tourney-2020/Week 8/Bambi/metal-calculus-OM2020_W8_MetalCalculus-6.solution
0.21147 C=109276 A=54 test/solution/tourney-2020/Week 8/rolamni/rolamni_OM2020_W8_A.solution
0.14588 C=7661 A=865 test/solution/tourney-2020/Week 8/RP0/metal-calculus-OM2020_W8_MetalCalculus-3.solution
0.57794 C=138691 A=174 test/solution/tourney-2020/Week 0/jinyou/940_OM2020_W0_ConnectTheDots_JINYOU_2310_485G_138691C_174A_80I_24P.solution
Before After After %
All entries 30.68476 29.19438 95.14%
>0.1 sec 18.73517 17.67972 94.37%

Test timings show a >5% speedup for large solutions.
@ianh

ianh commented Apr 1, 2026

Copy link
Copy Markdown
Owner

This code isn't like that for no reason; it has been carefully copied from a decompiled version of the game's actual collision detection functions, and it should perform the same operations (including strange casts to and from double, unnecessary calls to sqrt, and so on). I'm reluctant to make changes like this without validating that the faster code behaves identically to the slower code.

@d0sboots

d0sboots commented Apr 1, 2026

Copy link
Copy Markdown
Author

Hmm. That will be difficult to do, because the only place that you could see a difference between sqrt(x) < y and x < y * y is at the edges of numerical precision, which by definition are very difficult to test for. They're also very difficult to maintain even if you're sticking to the same operations - from trivially poking around, it looks like OM is C#, and omsim is C. You are dealing with different standard libraries, so you can't even guarantee that sqrt is returning the same values here across the two apps.

@ianh

ianh commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Good catch on the compiler warning, though; I just landed a more thorough fix in 7a5de77.

@d0sboots

d0sboots commented Apr 1, 2026

Copy link
Copy Markdown
Author

To elaborate more on my previous point: Although sqrt is required to be correctly rounded, and so almost certainly you will get the same values between different C standard libraries, as well as between C and C#, the same is very much not true of sin and cos, which have basically no guarantees. Linking to a different version of libc can and will produce different output, which can/will be different than what C# produces. Trying to be bit-exact in all cases here is IMO not possible.

@ianh

ianh commented Apr 1, 2026

Copy link
Copy Markdown
Owner

I believe C# calls into the C math functions in its runtime implementation. So using platform sin/cos is "as bit exact as possible", which was my goal here.

@ianh

ianh commented Apr 1, 2026

Copy link
Copy Markdown
Owner

Anyway, I am definitely interested in changes like this, but the reason I haven't done it is because I want to be able to say the code works (at least in the details of how the numbers are computed) the same way as the code in the game.

@d0sboots

d0sboots commented Apr 1, 2026

Copy link
Copy Markdown
Author

including strange casts to and from double

These are common in code written in languages like C or C# where the default floating-point type is double but developers try to use float "for speed" and don't know what they are doing. And yes, in general the behavior is different from just using float (which is why the compiler left them in), but in these cases it's provably the same:

roundf vs round: The argument is float, so promoting the argument to double does nothing. The result is immediately cast to int, so the extra precision of a double result is not relevant.

sqrtf vs sqrt: The sqrt functions are guaranteed to return the correctly-rounded result of the infinite-precision operation. Due to the nature of square-root, it is impossible for it to have a finite binary significand of bit length L without having a significand of length 2L for the input; thus, if the output is finite, it must fit in a float, and if it is infinite it will be correctly rounded. If it fits in a float, it fits in a double and thus the conversion will be a no-op; if it is infinite then the correctly-rounded double result will correctly round to the same as the correctly rounded float result during down-conversion.

@d0sboots

d0sboots commented Apr 1, 2026

Copy link
Copy Markdown
Author

I believe C# calls into the C math functions in its runtime implementation. So using platform sin/cos is "as bit exact as possible", which was my goal here.

Right, it's as exact as possible, but that still may not be exact. Because OM runs on Windows and the stdlib it's linking against is the Windows ones, while a common scenario for omsim would be linking against a linux libc (for instance). If you are not actively testing the edge cases already, you do not have any of the guarantees you are hoping for.

(I have a relatively deep experience with floating-point reproducibility and knowledge of people working on games having issues with it, especially in a multi-platform context, so I'm not just blowing smoke here.)

@d0sboots

d0sboots commented Apr 1, 2026

Copy link
Copy Markdown
Author

To be clear, I don't want to knock what you have. You have a robust set of test cases to verify the behavior, which IMO is exactly what you should have. But what you are saying you want is something much, much stronger, which is bit-accuracy to the game. This implies being exactly correct in all of the extremely buggy corner cases of the game, such as getting overclocking exactly right and farlands collision exactly right. Not only is that very hard, it's also unnecessary (you're forbidding such solutions already).

@ianh

ianh commented Apr 2, 2026

Copy link
Copy Markdown
Owner

To be honest, I'd love a way to actually test all the gnarly edge cases. The roundf/round sqrtf/sqrt changes do seem like they'd be fine; thanks for actually thinking them through. I'm not convinced exactness is unnecessary outside of stuff like the farlands, though. You just need to get really unlucky with a swing and you end up computing its area slightly wrong. Maybe there's a good argument for why that can't happen, though?

@d0sboots

d0sboots commented Apr 2, 2026

Copy link
Copy Markdown
Author

I'm certain that it can happen; you just need a careful computer search to find the right set of parameters. Probably such a thing would be excellent fodder for further more-robust tests. :)

@d0sboots

d0sboots commented Apr 2, 2026

Copy link
Copy Markdown
Author

Specifically, the issue is that sqrt loses information. Simple demonstration:

>>> for i in range(10):
     j = math.nextafter(1, 2, steps=i)
    print(j, math.sqrt(j))

1.0 1.0
1.0000000000000002 1.0
1.0000000000000004 1.0000000000000002
1.0000000000000007 1.0000000000000002
1.0000000000000009 1.0000000000000004
1.000000000000001 1.0000000000000004
1.0000000000000013 1.0000000000000007
1.0000000000000016 1.0000000000000007
1.0000000000000018 1.0000000000000009
1.000000000000002 1.0000000000000009

As a result, at the very edges there will be differences between sqrt(x) < y and x < y * y:

>>> for i in range(10):
...     j = math.nextafter(1.5, 2, steps=i)
...     k = math.sqrt(j)
...     print(j, j < k * k)
...
1.5 False
1.5000000000000002 False
1.5000000000000004 False
1.5000000000000007 True
1.5000000000000009 False
1.500000000000001 True
1.5000000000000013 False
1.5000000000000016 False
1.5000000000000018 True
1.500000000000002 False

@ianh

ianh commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Interestingly, at least on my computer, the most common case of atom/atom collision does seem to pass an exhaustive check:

#include <inttypes.h>
#include <math.h>
#include <stdint.h>
#include <stdio.h>

static const float atomRadius = 29;

static int check1(float f)
{
    return sqrtf(f) < 2 * atomRadius;
}

static int check2(float f)
{
    return f < 4 * atomRadius * atomRadius;
}

int main()
{
    uint32_t i = 0;
    do {
        union { uint32_t i; float f; } u = { .i = i };
        if (isnan(u.f) || u.f < 0)
            continue;
        if (check1(u.f) != check2(u.f))
            printf("%" PRIu32 " - %f (%d %d)\n", i, u.f, check1(u.f), check2(u.f));
    } while (++i != 0);
}

@d0sboots

d0sboots commented Apr 2, 2026

Copy link
Copy Markdown
Author

According to my math, one number should fail for 20 + 20.

Edit: Also 15 + 29

@ianh

ianh commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Yeah, I was just typing out this comment -- testing all the different combinations of atoms with other things, there seems to be a single problematic squared distance: 1935.999878 (0x44F1FFFF). This would give a spurious collision between a normal atom (radius 29) and an atom spawning from a glyph (radius 15).

@ianh

ianh commented Apr 2, 2026

Copy link
Copy Markdown
Owner

Using the faster check only for 29 + 29 would probably still be a substantial speed-up, though!

@d0sboots

d0sboots commented Apr 2, 2026

Copy link
Copy Markdown
Author

I think the first step now that we know about this would be to try and construct a test that creates that exact distance, and see what happens in-game. :D

@d0sboots

d0sboots commented Apr 2, 2026

Copy link
Copy Markdown
Author

(I see that the code does do some tests of the form x * x < y in other places, so the result may not be consistent. Only tests can really validate.)

@12345ieee

Copy link
Copy Markdown
Contributor

I tried too to get this in in #10 , good luck.

@d0sboots

d0sboots commented Apr 2, 2026

Copy link
Copy Markdown
Author

OK, so first step, I did a search for all floats of the form x^2 + y^2 == 1935.999878, and the candidates are pretty dense within the possible range. So, constructing an example seems at least theoretically possible.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants