FIX avoid non-inlined isnan() calls in tree/HGB hot loops - #34876
FIX avoid non-inlined isnan() calls in tree/HGB hot loops#34876cakedev0 wants to merge 8 commits into
Conversation
isnan() is supposed to always be inlined by the compiler (C11 type-generic macro -> __builtin_isnan*), but on some libc versions (e.g. glibc 2.17, used by conda-forge's sysroot for binary compatibility) it expands to a call to non-builtin, non-inlinable internal symbols (__isnanf/__isnan) instead. This turns a 2-instruction comparison into a real out-of-line function call, sitting in the hottest loops of tree building (DensePartitioner.find_min_max, .sort_samples_and_feature_values) and HGB binning/prediction. Replace isnan(value) with the self-inequality NaN test (value != value), which is mathematically equivalent and is pure comparison: there is nothing to inline, on any compiler or libc. See scikit-learn#34869 for the full investigation (this explains most of the reported ExtraTrees fit slowdown on conda-forge builds vs PyPI).
Co-Authored-By: Claude Sonnet 5 <[email protected]>
Co-Authored-By: Claude Sonnet 5 <[email protected]>
|
Would defining our own Maybe there are still some cases where it wouldn't be inlined but chatting with an AI (chAItting 😁) seems to tell me that our own inline Cython function is more likely to be inlined than |
|
That's a very good idea, and very safe for inlining yes. There is How to name it? |
|
isnan is fine. Just add a docstring that explains why it's better to use this than calling the libm library function. |
|
I added |
|
Apparently, there is a risk that aggressive floating-point optimization flags remove completely I added a test too, it'll fail if one day someone switches to |
|
Does using If that works, then I guess it's preferable to rely on numpy to do this kind of low-level tricks, rather than rolling our own "solution". Apparently numpy use the compiler builtin isnan if it exists, and AI is telling me it does at least for gcc and clang (MSVC maybe not). /* use builtins to avoid function calls in tight loops |
|
My AI says: I dug into this — the suggestion doesn't actually hold up for scikit-learn. Here's why: What numpy's #ifdef HAVE___BUILTIN_ISNAN
#define npy_isnan(x) __builtin_isnan(x)
#else
#define npy_isnan(x) isnan(x)
#endif
So for scikit-learn, |
|
OK thanks for trying, I still have a small preference for having a different name for our custom Happy to be overriden on this one, if I am the only one who thinks this 😉. I did see above @ogrisel thinks using the Suggestions:
We could also copy what numpy does with using compiler builtin isnan maybe? |
|
Fine with either. |
|
I'm putting back this PR as draft:
|
|
I chose As discussed IRL yesterday, let's do this fix rather than the glibc bump. |
Reference Issues/PRs
Fixes #34869
What does this implement/fix? Explain your changes.
As suggested in the issue, I replaced
isnan(value)with the self-inequality NaN test (value != value) where relevant.value != valueis mathematically equivalent toisnan(value)for IEEE 754 floats and is pure comparison. There's nothing to inline, on any compiler or libc, ever.Benchmarked against the exact real conda-forge toolchain,
ExtraTreesRegressor.fit, same case as #34869:main)The unfixed rebuild also lands right on the real published package's number (3292 vs 3257ms), confirming the reproduction is faithful.
A small residual gap remains (2398 vs 2154ms) that isn't explained by this specific bug, maybe noise. Anyway, not chasing it further here.
AI usage disclosure
I used AI assistance for every steps.