Int32 против Float64 выступлений в Crystal - PullRequest
2 голосов
/ 29 марта 2020

Я выполнил этот тест и был очень удивлен, увидев, что производительность Crystal практически одинакова для операций Int32 или Float64.

$ crystal benchmarks/int_vs_float.cr --release
  int32 414.96M (  2.41ns) (±14.81%)  0.0B/op        fastest
float64 354.27M (  2.82ns) (±12.46%)  0.0B/op   1.17× slower

Есть ли у меня какие-то странные побочные эффекты на моем коде теста?

require "benchmark"

res = 0
res2 = 0.0

Benchmark.ips do |x|
  x.report("int32") do
    a = 128973 / 119236
    b = 119236 - 128973

    d = 117232 > 123462 ? 117232 * 123462 : 123462 / 117232

    res = a + b + d
  end

  x.report("float64") do
    a = 1.28973 / 1.19236
    b = 1.19236 - 1.28973

    d = 1.17232 > 1.23462 ? 1.17232 * 1.23462 : 1.23462 / 1.17232

    res = a + b + d
  end
end

puts res
puts res2

1 Ответ

5 голосов
/ 30 марта 2020

Прежде всего / в Crystal - это деление на числа с плавающей точкой, так что это в значительной степени сравнивает числа с плавающей точкой:

typeof(a) # => Float64
typeof(b) # => Int32
typeof(d) # => Float64 | Int32)

Если мы установим эталонный тест для использования целочисленного деления, //, я получу:

  int32 631.35M (  1.58ns) (± 5.53%)  0.0B/op   1.23× slower
float64 773.57M (  1.29ns) (± 3.21%)  0.0B/op        fastest

По-прежнему нет реальной разницы в пределах погрешности. Почему это? Давайте копать глубже. Сначала мы можем извлечь примеры в не встроенную функцию и убедиться, что она вызывается так, что Crystal не просто игнорирует это:

@[NoInline]
def calc
  a = 128973 // 119236
  b = 119236 - 128973
  d = 117232 > 123462 ? 117232 * 123462 : 123462 // 117232

  a + b + d
end
p calc

Затем мы можем построить это с помощью crystal build --release --no-debug --emit llvm-ir, чтобы получить .ll файл с оптимизированным LLVM-IR. Мы выкапываем нашу calc функцию и видим что-то вроде этого:

define i32 @"*calc:Int32"() local_unnamed_addr #19 {
alloca:
  %0 = tail call i1 @llvm.expect.i1(i1 false, i1 false)
  br i1 %0, label %overflow, label %normal6

overflow:                                         ; preds = %alloca
  tail call void @__crystal_raise_overflow()
  unreachable

normal6:                                          ; preds = %alloca
  ret i32 -9735
}

Куда делись все наши вычисления? LLVM сделал их во время компиляции, потому что это были все константы! Мы можем повторить эксперимент на примере Float64:

define double @"*calc:Float64"() local_unnamed_addr #11 {
alloca:
  ret double 0x40004CAA3B35919C
}

Немного меньше шаблонного, следовательно, он немного быстрее, но опять же, все предварительно вычислено!

Я закончу упражнение Вот. Дальнейшие исследования для читателя:

  • Что произойдет, если мы попытаемся ввести непостоянные термины во все выражения?
  • Предполагается ли, что 32-битные целочисленные операции должны выполняться быстрее или медленнее, чем 64-битные IEEE754 операции с плавающей запятой на современном 64-разрядном процессоре в здравом уме?
...