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でのベンチマークを聞かせてもらったのですが、少し異常な感じの結果(とても遅い)だったので何か測定ミスの可能性もあり、ここへの掲載はとりあえず控えます。