地上の洞窟

どこにも行かず、液晶と「にらめっこ」し続ける人の物語。

Rubyで「マイナス乗」を計算しようとしてびびった話。

※古いRuby(1.9)のお話、新しいバージョンではマイナス乗とか気にしなくていい!

目次

前置き

n = 10
2 ** n

2のn乗。オーソドックスな書き方だ。
しかしRubyにおいて2のn乗を普通に書くのは
速度的にはあんましえらくない。とされている*1

1 << n

このように「ビットシフト」を用いて、「2のn乗」を高速化できる。

require "benchmark"

n = 32

Benchmark.bm(4) do |r|
  r.report("**") { 10_000_000.times { 2 ** n } }
  r.report("<<") { 10_000_000.times { 1 << n } }
end

#             user     system      total        real
# **     1.438000   0.000000   1.438000 (  1.434798)
# <<     1.093000   0.000000   1.093000 (  1.087205)

しかし、このビットシフトだと「小数点乗」とか「マイナス乗」が再現できない。

# これはできる

p 2 ** 3 # 8
p 1 << 3 # 8

# ここからおかしくなる

p 2 ** 3.5 # 11.313708498984761
p 1 << 3.5 # 8

p 2 ** -3 # (1/8)
p 1 << -3 # 0

じゃ~しょうがない
諦めて「**」で実装するかとやってみると

require "benchmark"

Benchmark.bm(4) do |r|
  r.report("<<") { 10_000_000.times { 1 << 5 } }
  r.report("**") { 10_000_000.times { 2 ** -5 } }
end

#            user     system      total        real
# <<     0.468000   0.000000   0.468000 (  0.465533)
# **     3.016000   0.000000   3.016000 (  3.014047)

何これ遅ッッッッッッッ

改善

x = 2  # 底
y = -5 # 指数

y >= 0 ? x ** y : 1.0 / (x ** y.abs)

とりあえずこんな感じの式で速くできる。
(元と違って分数クラスは中継しない)

検証↓

require "benchmark"

x = 2
y = -5

Benchmark.bm(4) do |r|
  r.report("**") { 10_000_000.times { x ** y } }
  r.report("kai"){ 10_000_000.times { y >= 0 ? x ** y : 1.0 / (x ** y.abs) } }
end

#            user     system      total        real
# **     3.125000   0.000000   3.125000 (  3.129178)
# kai    1.157000   0.000000   1.157000 (  1.158395)

まだ遅いけど相当マシ。

結論

古いRubyつらたん。

用意が面倒なのでWandboxを使って
Ruby 2.7.3辺りで同じことを試してみたが
こんなややこしいことをしなくても普通に速い。

余談

2^nは「<<」で書けという話だったが
2^5くらいだとなぜか「**」の方が速い逆転現象が起こる(?)

require "benchmark"

n = 5

Benchmark.bm(4) do |r|
  r.report("<<") { 10_000_000.times { 1 << n } }
  r.report("**") { 10_000_000.times { 2 ** n } }
end

#            user     system      total        real
# <<     0.500000   0.000000   0.500000 (  0.503168)
# **     0.484000   0.000000   0.484000 (  0.483344)

そして、「自作のBenchmark」で同じことをやると逆転しない。
(色々と仕様違うけど気にしないで)

module MyBenchmark
  class << self
    def run(count = 1)
      @count = count
      yield self if block_given?
    end

    def report(name = nil)
      if block_given?
        time = Time.now
        t = 0
        while t < @count
          yield
          t += 1
        end
        r = Time.now - time
        puts "#{name ? "#{name}: " : ""}#{r} s / #{(r / @count.to_f) * 6000} %"
      end
    end
  end
end

MyBenchmark.run(10_000_000) do |r|
  r.report("<<") { 1 << n }
  r.report("**") { 2 ** n }
end

# <<: 0.562924 s / 0.0003377544 %
# **: 0.618259 s / 0.0003709554 %

どっちを信じればいいんだろう?
まぁ、少なくとも「左シフト」に関しては使っていいと思う

右シフトは低い数だと本当に要らない。

require "benchmark"

Benchmark.bm(2) do |r|
  r.report(">>") { 10_000_000.times { 32 >> 5 } }
  r.report("/")  { 10_000_000.times { 32 / 32 } }
end

#          user     system      total        real
# >>   0.469000   0.000000   0.469000 (  0.470105)
# /    0.312000   0.000000   0.312000 (  0.322468)

*1:「**」は2^30あたりから急に遅くなる