これは何?
先日まで Mid 2015 の 15 inch MacBook Pro (Core i7 クアッド / 2.2 GHz) を使っていた。
先日 MacBook Pro 14 inch (M1 非Max) を手に入れたんだけど、あんまり速くないなと思うことがあったので、今日も楽しいマイクロベンチマーク。
計算内容
ruby で書くと短くていいね。
N=10000
r=(1..N).max_by{ |x| ((N-x)**x/7) % 6074001001 }
p r
こういう内容。なんの意味もない。
出力は
8663
となれば正解。
これを、go, java, c++ with boost (clang, gcc), ruby, python3, node で試した。
以降、グラフで出てくる "m1", "rosetta", "amd64" の意味は下表のとおり。
記号 | 実行ハードウェア | バイナリ |
---|---|---|
m1 | MacBook Pro 14 inch (M1 非Max) | arm64 |
rosetta | MacBook Pro 14 inch (M1 非Max) | x86_64 |
amd64 | MacBook Pro (Core i7, Mid 2015) | x86_64 |
コンパイルするチーム
go, java, c++ with boost (clang, g++-11)。
各コンパイラは下記の通り
- go version go1.17.5 darwin/arm64
- openjdk 17.0.1 2021-10-19 LTS
- Apple clang version 13.0.0 (clang-1300.0.29.30)
- g++-11 (Homebrew GCC 11.2.0_3) 11.2.0
java と clang の rosetta はサボった。
結果は下記の通り。 time コマンドの real の値を出しているので棒が短いほど速い。
ちなみに、 real なのは並列実行を優遇するため。実際、 Java は user が real の 1.5倍ぐらいある。
結果は下図。
目盛りを見ると分かる通り、 go が速い。意外と clang が M1 を使いこなせてない感じ。
全体的にはまあそうだよねという結果だと思う。
コンパイルしないチーム
続いて、 ruby, python3, node。
各環境は下記の通り
- ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [arm64-darwin21] / for m1
- ruby 3.0.3p157 (2021-11-24 revision 3fb7d2cadc) [x86_64-darwin21] / for rosetta
- ruby 3.1.0p0 (2021-12-25 revision fb4df44d16) [x86_64-darwin21] / for amd64
- Python 3.9.10 (main, Jan 15 2022, 11:40:53) / for m1
- Python 3.9.10 (main, Jan 15 2022, 11:48:04) / for rosetta
- Python 3.9.10 (main, Jan 15 2022, 11:48:04) / for amd64
- node v17.3.0 / for m1 and rosetta
- node v17.3.1 / for amd64
なんか ruby と node のバージョンが合ってないけど気にしない。
結果は下図。
こちらはわりと思いがけなかった。
node はまあまあそうだよねという内容。m1 と amd64 の差はもうちょっとあってもいいかなと思うけど。
python3 は、三者ほぼ同タイム。
そして ruby は m1 が一番遅いという意外な展開。よく見てみると、m1 が遅いというより、amd64 が速すぎる。amd64 の中では go と並んでほぼ最速。m1 が遅いと書いたもののそれは ruby 内の比較の話。m1 内での比較だと Java と同等、clang より速い。node には負けるけど。
ruby や python で m1 がふるわないのは、おそらく、x86_64 バイナリは SSEとかをたっぷり使っていて、ARM64 バイナリは NEONとかを使いこなせてないんだろうと想像する。調べてないので想像するだけ。
誤解なきよう
ここでやっているのはマイクロベンチマーク。多倍長整数の特定の計算だけしかしていない。
「ruby は M1 でも遅いのか」という感想を持つべきではなくて「ruby は多倍長整数計算では M1 でも遅いことがあるのか」という感想が正解。
実際。
多倍長整数ではない計算をすると、私が試した範囲では全部、M1 は rosetta に圧勝する。
時間測定に使ったコード
# hash
require "json"
$ix=0
def foo(x)
return { ($ix+=1).to_i=>($ix+=1).to_s } if x==0
foo(x-1).merge(foo(x-1))
end
p foo((ARGV[0]||21).to_i).size
# json
require "json"
def foo(x)
x.times.with_object({}){ |e,o| o[e] = JSON.parse(foo(x-1)) }.to_json
end
p foo((ARGV[0]||9).to_i).size
# eval long text
def foo(s, n)
return eval(s) if n==0
foo("(#{s})*2+(#{s})", n-1)
end
p foo("1", (ARGV[0]||21).to_i)
# many delete_at
def foo(n)
a=[*1..n]
(1..).each do |ix|
return a[0] if a.size==1
a.delete_at(ix % a.size)
end
end
p foo((ARGV[0]||200000).to_i)
# float calc
def foo(n)
n.downto(1).sum{ |e|
x=e.to_f
(x+1)/(x**(x**0.1))
}
end
p foo((ARGV[0]||10000000).to_i)
# regexp
def foo(n)
s = "___"+(1..n).map{ |x| "o"*x }.join("___")+"___"
s.scan(/(_(o(o+))(\2+)_)/).sum{ |e| e[0].size-2}
end
p foo((ARGV[0]||4096).to_i)
# deep flatten
def foo(n)
return [1] if n<1
[foo(n-1)*n]*n
end
p foo(ARGV[0] || 7).flatten.sum
# recursive fibo
def foo(n)
return n if n<2
foo(n-1) + foo(n-2)
end
p foo(ARGV[0] || 37)
上記の折りたたんであるコードを実行すると、下図のとおり、 M1 が勝つ。
まあそりゃそうだ、という話。
まとめ
M1 ネイティブでも rosetta 2 より遅いこともあるよ。
とはいえ。ほとんどの場合は M1 ネイティブは速いし、今遅いものもそのうち早くなるんじゃないかと思うよ(思うだけ)。