2022/01/13(木)M1 macの区間演算は遅い?
g++11 -O3 -I(kvのあるとこ) -I/opt/homebrew/include -L/opt/homebrew/lib hogehoge.cとかすれば動くようになりました。
で、なぜか区間演算が異常に遅いことに気付きました。とりあえず区間演算のベンチマークとして、
#include <iostream> #include <kv/interval.hpp> #include <kv/rdouble.hpp> int main() { int i; kv::interval<double> x; x = 1; for (i=0; i<1000000000; i++) { x = (x*x-3)/(x+x); } std::cout << x << std::endl; }みたいなのを使います。加減乗除を一回ずつ含んでいて、[1,1]と[-1,-1]を交互に繰り返します。本当は変なベンチマークテストみたいにいろんな値を取る写像を使いたかったのですが、区間演算だとすぐに発散して[-∞, ∞]になってしまい、ベンチマークとしてふさわしくないものになってしまいました。
これの実行時間を、ThinkPad X1 carbon (core i7 10510U, ubuntu 20.04, gcc 9.3)で計測すると、-O3で38.1秒程度でした。この状態では、丸めの向きの変更はfesetroundを使っています。kvライブラリには、Intelの64bit CPUのとき-DKV_FASTROUNDを付けるとfesetroundを使わずにSSE2の丸めの向きを制御するmxcsrレジスタに直接書き込むことによって高速化する機能があります。このときは、19.4秒と倍近くに高速化しました。更に、-DKV_NOHWROUNDを付けるとハードウェアによる丸めの向きの変更は一切行わず、twosumとtwoproductを用いて方向付き丸めのエミュレートを行わせることができます。このときは、35.7秒程度でした。
で、これをMacBook Air (M1, Monterey 12.1, gcc 11.2) で実行してみると、-O3でなんと142秒もかかってしまいました。fesetroundが重いのかと思い、aarch64なCPUに対しても-DKV_FASTROUNDが使えるようにkvを改造し(次期kv-0.4.55で入れる予定)実行してみると、114秒といくらか改善したものの、まだまだ遅いです。で、-DKV_NOHWROUNDだと、なんと23.8秒と劇的に改善しました。方向付き丸めのエミュレーションはかなり複雑な計算をしていて、こんなに速いはずはないのですが。まとめると、次のような感じです。
-O3 | -O3 -DKV_FASTROUND | -O3 -DKV_NOHWROUUND | |
---|---|---|---|
X1 carbon | 38.1 | 19.4 | 35.7 |
M1 MBA | 142 | 114 | 23.8 |
そこで、次のような実験をしてみました。変なベンチマークテストのベンチマークに毎回丸めの向きの変更を挿入してみます。fesetroundの実装に影響されないように、アセンブリ埋め込みの最速と思われる実装にします。
#include <stdio.h> #include <stdint.h> int main() { int i; double x; #ifdef __aarch64__ uint64_t reg; uint64_t regs[4]; asm volatile ("mrs %0, fpcr" : "=r" (reg)); regs[0] = (reg & ~(3ULL << 22)) | (0ULL << 22); // nearest regs[1] = (reg & ~(3ULL << 22)) | (2ULL << 22); // down regs[2] = (reg & ~(3ULL << 22)) | (1ULL << 22); // up regs[3] = (reg & ~(3ULL << 22)) | (3ULL << 22); // chop #endif #ifdef __x86_64__ uint32_t reg; uint32_t regs[4]; asm volatile ("stmxcsr %0" : "=m" (reg)); regs[0] = (reg & ~(3UL << 13)) | (0UL << 13); // nearest regs[1] = (reg & ~(3UL << 13)) | (1UL << 13); // down regs[2] = (reg & ~(3UL << 13)) | (2UL << 13); // up regs[3] = (reg & ~(3UL << 13)) | (3UL << 13); // chop #endif x = 0.5; for (i=0; i< 1000000000; i++) { #ifdef __aarch64__ #ifdef ROUND_CHANGE1 asm volatile ("msr fpcr, %0" : : "r" (regs[0])); #endif #ifdef ROUND_CHANGE2 asm volatile ("msr fpcr, %0" : : "r" (regs[i%4])); #endif #endif #ifdef __x86_64__ #ifdef ROUND_CHANGE1 asm volatile ("ldmxcsr %0" : : "m" (regs[0])); #endif #ifdef ROUND_CHANGE2 asm volatile ("ldmxcsr %0" : : "m" (regs[i%4])); #endif #endif x = 1 / (x * (x - 1)) + 4.6; } printf("%g\n", x); }埋め込んだ丸め変更命令に2種類あって、
- -DROUND_CHANGE1 丸め変更命令を埋め込むが、丸めの向きはずっとnearest
- -DROUND_CHANGE2 丸めの向きをnearest->down->up->chopと周期的に変更
-O3 | -O3 -DROUND_CHANGE1 | -O3 -DROUND_CHANGE2 | |
---|---|---|---|
X1 carbon | 6.43 | 6.44 | 6.53 |
M1 MBA | 6.29 | 6.29 | 15.04 |
なお、これはaarch64なアーキテクチャ全般に見られる傾向かどうかが気になったので、3万円のchromebook (Lenovo IdeaPad Duet Chromebook, MediaTek Helio P60T) で試してみたら、
-O3 | -O3 -DROUND_CHANGE1 | -O3 -DROUND_CHANGE2 | |
---|---|---|---|
IdeaPad Duet | 15.8 | 20.7 | 20.9 |
ARMでのベンチマーク追加 (2022/6/30追記)
- OCI (Oracle Cloud Infrastructure)のAlways Free ARMで提供されている、Ampere Altraプロセッサ。Ubuntu 20.04、gcc 9.4.0。
- Planexのスマートフォン、Gemini PDA (CPUはMediatek MT6797X Helio X27)にTermuxで入れたclang 14.0.1
-O3 | -O3 -DROUND_CHANGE1 | -O3 -DROUND_CHANGE2 | |
---|---|---|---|
X1 carbon | 6.43 | 6.44 | 6.53 |
M1 MBA | 6.29 | 6.29 | 15.04 |
IdeaPad Duet | 15.8 | 20.7 | 20.9 |
Ampere Altra | 6.69 | 6.69 | 9.70 |
Gemini PDA | 19.7 | 23.0 | 28.8 |
なお、とある方から富岳のA64FXでのベンチマークを聞かせてもらったのですが、少し異常な感じの結果(とても遅い)だったので何か測定ミスの可能性もあり、ここへの掲載はとりあえず控えます。