Search code examples
performancecrystal-lang

Can this Crystal benchmark code be improved significantly?


I'm deciding on a language to use for back-end use. I've looked at Go, Rust, C++, and I thought I'd look at Crystal because it did reasonably well in some benchmarks. Speed is not the ultimate requirement, however speed is important. The syntax is equally important, and I'm not averse to the Crystal syntax. The simple benchmark that I wrote is only a small part of the evaluation and it's also partly familiarisation. I'm using Windows, so I'm using Crystal 0.35.1 on Win10 2004-19041.329 with WSL2 and Ubuntu 20.04 LTS. I don't know if WSL2 has any impact on performance. The benchmark is primarily using integer arithmetic. Go, Rust, and C++ have almost equal performance to each other (on Win10). I've translated that code to Crystal, and it runs a fair bit slower than those three. Out of simple curiosity I also ran the code on Dart (on Win10), and it ran (very surprisingly) almost twice as fast as those three. I do understand that a simple benchmark does not say a lot. I notice from a recent post that Crystal is more efficient with floats than integers, however this was and is aimed at integers.

This is my first Crystal program, so I thought I should ask - is there any simple improvements I can make to the code for performance? I don't want to improve the algorithm other than to correct errors, because all are using this algorithm.

The code is as follows:

# ------ 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}"


Solution

  • You need to compile with --release flag. By default, the Crystal compiler is focused on compilation speed, so you get a compiled program fast. This is particularly important during development. If you want a program that runs fast, you need to pass the --release flag which tells the compiler to take time for optimizations (that's handled by LLVM btw.).

    You might also be able to shave off some time by using wrapping operators like &+ in location where it's guaranteed that the result can'd overflow. That skips some overflow checks.


    A few other remarks:

    Instead of 0.to_i64, you can just use a Int64 literal: 0_i64.

    iMills = uninitialized Int32
    

    Don't use uninitialized here. It's completely wrong. You want that variable to be initialized. There are some use cases for uninitialized in C bindings and some very specific, low-level implementations. It should never be used in most regular code.

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

    Where did you read that?

    Adding type prefixes to identifiers doesn't seem to be very useful in Crystal. The compiler already keeps track of the types. You as the developer shouldn't have to do that, too. I've never seen that before.