Int32 与 Float64 在 Crystal 中的表现
Int32 vs Float64 performances in Crystal
我 运行 这个基准,我很惊讶地看到 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
首先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
构建它以获得具有优化的 LLVM-IR 的 .ll
文件。我们挖掘出我们的 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
}
样板少一点,因此速度稍快,但同样,所有都是预先计算的!
练习到此结束。进一步研究 reader:
- 如果我们尝试在所有表达式中引入非常量项会怎样?
- 在现代 64 位 CPU 上 32 位整数运算应该比 64 位 IEEE754 浮点运算快或慢的前提是正常的吗?
我 运行 这个基准,我很惊讶地看到 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
首先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
构建它以获得具有优化的 LLVM-IR 的 .ll
文件。我们挖掘出我们的 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
}
样板少一点,因此速度稍快,但同样,所有都是预先计算的!
练习到此结束。进一步研究 reader:
- 如果我们尝试在所有表达式中引入非常量项会怎样?
- 在现代 64 位 CPU 上 32 位整数运算应该比 64 位 IEEE754 浮点运算快或慢的前提是正常的吗?