PERF: `-ftrapping-math` slows down `np.less` `float32` loops on Apple AArch64

オープン
#32,159 コメント 4 件 リアクション 0 件 担当者 0 名 GitHub で見る

まだ誰も着手していません。

評価

難易度
4/5
見積もり時間
3〜5日
初心者へのやさしさ
48/100
issue の種類
バグ
明瞭さ
おおむね明確
活発さ
静か
技術スタック
python
領域
performance

調査の方向性

まず、提供されている np.less および関連する ufunc のベンチマークを Apple AArch64 で再現し、-ftrapping-math を付けたビルドと付けないビルドを比較します。ビルドフラグが no_float_errors および SIMD 比較とどのように相互作用するかを調査し、その後、フラグを削除または制限しても、divide の例を含む浮動小数点エラーの挙動が維持されるかを確認します。必要な例外処理を変更せずに、測定可能なパフォーマンス向上が得られれば完了です。

索引モデルが issue の本文から書いたものです。

説明

00 - Bug
Describe the issue:

Default numpy 2.5.1 build passes -ftrapping-math to every compiled file.
That numpy version is built with apple clang 15 (based on llvm 16), which is where AArch64 started honoring this flag.
With this flag, llvm scalarizes comparisons, because the exception flags for vectorized and scalar comparisons are different.
But for fcmeq this is unnecessary (llvm issue). And in comparisons that use fcmge and others, numpy sets no_float_errors=True,
so apart from hurting performance and toggling NPY_SIMD_GUARD_PARTIAL_LOAD, this flag does nothing for these ufuncs.

If you remove scalarization, for example by forcing -fno-trapping-math(spin build --clean -- -Dc_args=-fno-trapping-math -Dcpp_args=-fno-trapping-math) to measure
(NPY_SIMD_GUARD_PARTIAL_LOAD won't change because no-trap doesn't cancel -DNPY_HAVE_CLANG_FPSTRICT), you'll get:

Here the time is measured for 5e8 elements:

lambda f: min(timeit.repeat(f, repeat=10, number=int(5e8/n)))
n type func trap no-trap
100000 float32 less 0.1024202 0.0326608
100000 float32 less_equal 0.1025368 0.0305651
100000 float32 add 0.0309067 0.0326873
100000 float32 minimum 0.0307194 0.0317346
100000 float32 fmin 0.0307535 0.0329900
100000 float32 sin 0.5513217 0.3256507
100000 float32 exp 0.5220611 0.5224958
100000 float32 sqrt 0.0616653 0.0617320
100000 float64 less 0.0898140 0.0646568
100000 float64 less_equal 0.0897867 0.0639634
100000 float64 add 0.0880477 0.0912926
100000 float64 minimum 0.0790273 0.0892458
100000 float64 fmin 0.0792423 0.0884805
100000 float64 sin 1.2245625 1.2364713
100000 float64 exp 0.6959067 0.6961037
100000 float64 sqrt 0.1165173 0.1165576
10000000 float32 less 0.1020606 0.0473397
10000000 float32 less_equal 0.1021814 0.0473080
10000000 float32 add 0.0509410 0.0516631
10000000 float32 minimum 0.0515318 0.0516390
10000000 float32 fmin 0.0515197 0.0516493
10000000 float32 sin 0.5514753 0.3254238
10000000 float32 exp 0.5212769 0.5213362
10000000 float32 sqrt 0.0609508 0.0609891
10000000 float64 less 0.0918725 0.0962532
10000000 float64 less_equal 0.0925714 0.0962611
10000000 float64 add 0.1034192 0.1042124
10000000 float64 minimum 0.1044532 0.1040570
10000000 float64 fmin 0.1043755 0.1036816
10000000 float64 sin 1.3308548 1.3272675
10000000 float64 exp 0.6959297 0.6958054
10000000 float64 sqrt 0.1157437 0.1157453

Note that you can't do this, because divide will set the invalid flag on tail elements, although the data will be correct. But there's no divide in the benchmark, so the measurement is valid.

import numpy as np
np.seterr(all='raise')
for n in range(1, 33):
    a = np.arange(1, n + 1, dtype=np.float32)
    a / a

with -fno-trapping-math it gives FloatingPointError: invalid value encountered in divide.

So the question: is it necessary to set the flag everywhere, even for no_float_errors, since it impacts performance?

Measurements were on clang 21, apple m5, numpy 2.6.0.dev0. On apple clang 15 the measurements are slightly different, but the regression remains.

Reproduce the code example:
import numpy as np, timeit

print("n,type,func,time")
for n in (int(1e5), int(1e7)):
    for t in (np.float32, np.float64):
        a = np.random.rand(n).astype(t)
        b = np.random.rand(n).astype(t)
        cb = np.empty(n, np.bool)
        cb.fill(0)
        cf = np.empty(n, t)
        cf.fill(0)

        bench = lambda f: min(timeit.repeat(f, repeat=10, number=int(5e8/n)))

        for f in (np.less, np.less_equal):
            print(str(n) + "," + str(t.__name__) + "," + str(f.__name__ )+ "," + f"{bench(lambda: f(a, b, out=cb)):.7f}")
        for f in (np.add, np.minimum, np.fmin):
            print(str(n) + "," + str(t.__name__) + "," + str(f.__name__) + "," + f"{bench(lambda: f(a, b, out=cf)):.7f}")
        for f in (np.sin, np.exp, np.sqrt):
            print(str(n) + "," + str(t.__name__) + "," + str(f.__name__) + "," + f"{bench(lambda: f(a, out=b)):.7f}")
Error message:

Python and NumPy Versions:

2.6.0.dev0+git20260729.4350526
3.14.4 (v3.14.4:23116f998f6, Apr 7 2026, 09:45:22) [Clang 17.0.0 (clang-1700.6.4.2)]

Runtime Environment:

[{'numpy_version': '2.6.0.dev0+git20260729.4350526',
'python': '3.14.4 (v3.14.4:23116f998f6, Apr 7 2026, 09:45:22) [Clang 17.0.0 '
'(clang-1700.6.4.2)]',
'uname': uname_result(system='Darwin', node='MacBook-Pro.local', release='25.4.0', version='Darwin Kernel Version 25.4.0: Thu Mar 19 19:33:43 PDT 2026; root:xnu-12377.101.15~1/RELEASE_ARM64_T8142', machine='arm64')},
{'simd_extensions': {'baseline': ['NEON', 'NEON_FP16', 'NEON_VFPV4', 'ASIMD'],
'found': ['ASIMDHP', 'ASIMDDP'],
'not_found': ['ASIMDFHM', 'SVE']}},
{'ignore_floating_point_errors_in_matmul': True}]

How does this issue affect you or how did you find it:

I found a discrepancy in the speed of np.sin between 2.5.1 (clang 15) and 2.6.0 (clang 21), went to investigate and found that they both scalarize comparisons.

主要言語
Python
スター
32.8k
フォーク
12.8k
平均マージ
1日 7時間
マージ済み PR(30日)
197

コントリビューションガイド

コントリビューションガイドを開く

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

numpy/numpy のほかの issue

numpy/numpy の issue をすべて見る

似ている issue

Python の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。