这个 Crystal 基准代码可以得到显着改进吗?

Can this Crystal benchmark code be improved significantly?

我正在决定用于后端使用的语言。我看过 Go、Rust、C++,我想我会看看 Crystal,因为它在某些基准测试中表现相当不错。速度不是最终要求,但速度很重要。语法同样重要,我并不反对 Crystal 语法。我写的简单基准只是评估的一小部分,也是部分熟悉。我正在使用 Windows,所以我在 Win10 2004-19041.329 上使用 Crystal 0.35.1,WSL2 和 Ubuntu 20.04 LTS。不知道WSL2对性能有没有影响。基准测试主要使用整数运算。 Go、Rust 和 C++ 的性能几乎相同(在 Win10 上)。我已经 运行 将该代码指定为 Crystal,它的运行速度比这三个代码慢一点。出于简单的好奇心,我也 运行 Dart 上的代码(在 Win10 上),它 运行 (非常令人惊讶)几乎是这三个代码的两倍。我明白一个简单的基准并不能说明很多。我从最近的 post 中注意到 Crystal 使用浮点数比使用整数更有效,但是这是针对整数的。

这是我的第一个 Crystal 程序,所以我想我应该问 - 我可以对代码进行任何简单的改进以提高性能吗?除了纠错我不想改进算法,因为都在用这个算法

代码如下:

# ------ Prime-number counter. -----#

# Brian      25-Jun-2020       Program written - my first Crystal program.

# -------- METHOD TO CALCULATE APPROXIMATE SQRT ----------#

def fnCalcSqrt(iCurrVal)  # Calculate approximate sqrt
  iPrevDiv = 0.to_i64
  iDiv = (iCurrVal // 10)
  if iDiv < 2
    iDiv = 2
  end
  while (true)
    begin
      iProd = (iDiv * iDiv)
    rescue vError
      puts "Error = #{vError}, iDiv = #{iDiv}, iCurrVal = #{iCurrVal}"
      exit
    end
    if iPrevDiv < iDiv
      iDiff = ((iDiv - iPrevDiv) // 2)
    else
      iDiff = ((iPrevDiv - iDiv) // 2)
    end

    iPrevDiv = iDiv

    if iProd < iCurrVal # iDiv IS TOO LOW #
      if iDiff < 1
        iDiff = 1
      end
      iDiv += iDiff
    else
      if iDiff < 2
        return iDiv
      end
      iDiv -= iDiff
    end
  end
end

# ---------- PROGRAM MAINLINE --------------#

print "\nCalculate Primes from 1 to selected number"
#iMills = uninitialized Int32   # CHANGED THIS BECAUSE IN --release DOES NOT WORK
iMills = 0.to_i32
while iMills < 1 || iMills > 100
  print "\nEnter the ending number of millions (1 to 100) : "
  sInput = gets
  if sInput == ""
    exit
  end
  iTemp = sInput.try &.to_i32?
  if !iTemp
    puts "Please enter a valid number"
    puts "iMills = #{iTemp}"
  elsif iTemp > 100   # > 100m
    puts "Invalid - too big must be from 1 to 100 (million)"
  elsif iTemp < 1
    puts "Invalid - too small - must be from 1 to 100 (million)"
  else
    iMills = iTemp
  end
end

#iCurrVal = 2   # THIS CAUSES ARITHMETIC OVERFLOW IN SQRT CALC.
iCurrVal = 2.to_i64
iEndVal = iMills * 1_000_000
iPrimeTot = 0

# ----- START OF PRIME NUMBER CALCULATION -----#

sEndVal = iEndVal.format(',', group: 3) # => eg. "10,000,000"
puts "Calculating number of prime numbers from 2 to #{sEndVal} ......"
vStartTime = Time.monotonic
while iCurrVal <= iEndVal
  if iCurrVal % 2 != 0 || iCurrVal == 2
    iSqrt = fnCalcSqrt(iCurrVal)
    tfPrime = true  # INIT
    iDiv = 2
    while iDiv <= iSqrt
      if ((iCurrVal % iDiv) == 0)
        tfPrime = (iDiv == iCurrVal);
        break;
      end
      iDiv += 1
    end
    if (tfPrime)
      iPrimeTot+=1;
    end
  end
  iCurrVal += 1
end
puts "Elapsed time = #{Time.monotonic - vStartTime}"
puts "prime total = #{iPrimeTot}"

您需要使用 --release 标志进行编译。默认情况下,Crystal 编译器专注于编译速度,因此您可以快速获得已编译的程序。这在开发过程中尤为重要。如果你想要一个快速运行的程序,你需要传递 --release 标志,它告诉编译器需要时间进行优化(顺便说一句,由 LLVM 处理)。

您还可以通过在保证结果不会溢出的位置使用 &+ 等包装运算符来节省一些时间。这会跳过一些溢出检查。


其他几点:

而不是 0.to_i64,您可以只使用 Int64 文字:0_i64.

iMills = uninitialized Int32

这里不要使用uninitialized。这是完全错误的。您想要 初始化该变量。 uninitialized 在 C 绑定和一些非常具体的低级实现中有一些用例。它不应该在大多数常规代码中使用。

I notice from a recent post that Crystal is more efficient with floats than integers

你在哪里读到的?

向标识符添加类型前缀在Crystal中似乎不是很有用。编译器已经跟踪类型。作为开发人员,您也不应该这样做。我以前从没见过。