M1 Mac も速くないことがある

# これは何？

先日まで Mid 2015 の 15 inch MacBook Pro (Core i7 クアッド / 2.2 GHz) を使っていた。
先日 MacBook Pro 14 inch (M1 非Max) を手に入れたんだけど、あんまり速くないなと思うことがあったので、今日も楽しいマイクロベンチマーク。

# 計算内容

ruby で書くと短くていいね。

```ruby:ruby
N=10000
r=(1..N).max_by{ |x| ((N-x)**x/7) % 6074001001 }
p r
```
こういう内容。なんの意味もない。

出力は

```text
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_java_clang_gcc.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6854/c26ac009-258d-3343-d139-6b2f04092db5.png)

目盛りを見ると分かる通り、 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 のバージョンが合ってないけど気にしない。

結果は下図。

![ruby_python3_node.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6854/1b0e6e81-ca0c-4a88-13d7-8c91632ab889.png)

こちらはわりと思いがけなかった。

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 に圧勝する。
<details><summary>時間測定に使ったコード</summary><div>

```ruby:ruby
# 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
```

```ruby:ruby
# 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
```

```ruby:ruby
# 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)
```

```ruby:ruby
# 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)
```

```ruby:ruby
# 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)
```

```ruby:ruby
# 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)
```

```ruby:ruby
# deep flatten

def foo(n)
    return [1] if n<1
    [foo(n-1)*n]*n
end

p foo(ARGV[0] || 7).flatten.sum
```

```ruby:ruby
# recursive fibo

def foo(n)
    return n if n<2
    foo(n-1) + foo(n-2)
end

p foo(ARGV[0] || 37)
```


</div></details>

上記の折りたたんであるコードを実行すると、下図のとおり、 M1 が勝つ。
まあそりゃそうだ、という話。

![result.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/6854/00c17dcd-1c20-2092-6d05-3f25f4255b1c.png)
result.png

# まとめ

M1 ネイティブでも rosetta 2 より遅いこともあるよ。
とはいえ。ほとんどの場合は M1 ネイティブは速いし、今遅いものもそのうち早くなるんじゃないかと思うよ（思うだけ）。