これは何?
x86 は、32bit, 64bit の他に 80bit の浮動小数点数を計算する装置を内蔵していて、いずれもめちゃくちゃ速く計算できる。
一方。
Apple M1 は 80bitの浮動小数点数を計算する装置は持っていないので、80bit の浮動小数点数を計算するにはそうとう面倒なことをする必要がある。
じゃあさせてみよう、というのがこの記事の趣旨。
前提
C/C++ には普通三種類の浮動小数点数型がある。gcc なんかでは下表のとおりになっている。
型 | x86 / amd64 でのサイズ | Apple M1(arm64) でのサイズ |
---|---|---|
float | 32 | 32 |
double | 64 | 64 |
long double | 80 1 | 64 |
なので、M1 上でネイティブコードを動かしても 80bit では計算できない。
そこで、amd64 用の 80bit の浮動小数点数をつかうバイナリを作り、Rosetta2 を使って Apple M1 上で動かす。
実験
環境
マシンは手元にある下記の2台を使った。
- MacBook Pro 14 inch (M1 非Max)
- MacBook Pro (Core i7, Mid 2015)
ソースコード
そしてソースコード。
# include <stdio.h>
# include <stdlib.h>
# include <sys/time.h>
typedef NUM_T num_t;
long double test(int n)
{
num_t sum = 0;
num_t const one = 1.0;
for (int i = 1; i < n; ++i)
{
sum += (one / (i+1))*(one / i);
}
return sum;
}
int main(int argc, char const *argv[])
{
struct timespec start={0}, end={0};
clock_gettime(CLOCK_REALTIME, &start);
long double r = test(20000000);
clock_gettime(CLOCK_REALTIME, &end);
time_t ds = end.tv_sec - start.tv_sec;
time_t dn = end.tv_nsec - start.tv_nsec;
printf("sizeof(num_t) = %zu\n", sizeof(num_t));
printf("r=%.20Lf t=%f\n", r, ds + dn * 1e-9);
return 0;
}
除算・乗算・加算 を使った無駄な計算。
NUM_T
は、コンパイラオプションで定義するんだけど、 float
, double
, long double
のいずれか。
コンパイルオプション
コンパイルオプションは以下の通り。
全種類共通として -O2
, -std=c17
, -fexcess-precision=standard
を指定。
あとは下表。
設定名 | オプション |
---|---|
native | なし |
sse | -mfpmath=sse -msse2 |
387 | -mfpmath=387 |
実行結果
まずグラフ。縦軸は実行時間なので、上ほど遅い。縦軸対数目盛注意。
時間はグラフのとおりなんだけど、計算結果についてもひとつ。
double の場合、-mfpmath=387
と -mfpmath=sse -msse2
では計算結果が異なる。計算途中で 80bit を使うかどうかが違うので、差が出てしまう。下表のとおり。
設定名 | 浮動小数点関連オプション | 計算結果 |
---|---|---|
native | なし | r=0.99999995000372399190 |
sse | -mfpmath=sse -msse2 |
r=0.99999995000372399190 |
387 | -mfpmath=387 |
r=0.99999995000375718757 |
このあたりの話は 浮動小数点数の呪われた世界(x87 と C/C++) に書いた。
float でも差が出そうなものなんだけど、今回は差が出なかった。
感想とか
80bit 浮動小数点数の計算を Apple M1 でやるととても遅いことが確認できた。64bit と 80bit では、100倍以上遅い。
ソースコード上は long double を使っていなくても、 -mfpmath=387
をつけてしまうと途中の計算で 80bit 浮動小数点数を使ってしまうため、とても遅くなる。
float と double で、Apple M1 native (arm64 native) よりも rosetta (arm64 sse) の方が速いのは、Apple M1 向けだとコンパイラの最適化器が頑張りきれてないということだと思う。あるいは実行時最適化が走ったりするとか?
あと。
こういうときって何グラフを使うのがいいんだろう。
縦軸が対数目盛なので棒グラフは避けたい。
横軸がハードウェアとかコンパイラオプションなので折れ線グラフもありえない。
仕方ないので散布図にしたんだけど、なんか寂しい絵だなと思って。
ふつうどうするんだろ。
-
sizeof(long double) は 16 なので 128bit あるが、意味のあるデータは 80bit。 ↩