Repository navigation
Conversation
When the C extension is unavailable (JRuby or environments without a native compiler), load a pure Ruby implementation of Blowfish/bcrypt_pbkdf instead of raising. Approximately 68x slower than the C extension but acceptable for the one-time key-decryption use case in net-ssh.
BenchmarkTested on Apple M1 (arm64-darwin), Ruby 2.7.8 / JRuby 9.4.9.0 (OpenJDK 20). The pure Ruby fallback is ~30x slower than the Java extension and ~68x slower than the C extension. At This PR is intended as a stopgap so JRuby users aren't completely blocked. The right fix for JRuby performance is #30 (Java extension). |
|
I also looked into the perf of the extension, and there's a lot that can be improved. It copies incoming and outgoing values at least twice, uses Java integration for calls (copying the input again). I'm working on some fixes, but the first commit should probably be to clean up the tab characters that snuck in with the initial PR. |
|
@mfazekas Is that benchmark published somewhere? I would like to do more profiling but I think I had the wrong code in hand before. Glancing through the correct core submitted by @kares I can definitely see opportunities (as mentioned above, reducing copying and transient object allocations would probably have a big impact). |
|
Tested on Apple M2 Max, MRI 2.7.8 / JRuby 9.4.9.0 (OpenJDK 20). Java ext is ~1.35× slower than C ext. Pure Ruby is ~34–38× slower than Java ext on JRuby. bench/pure_ruby_bench.rbrequire 'benchmark'
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
native = ENV['NATIVE'] != '0'
if native
require 'bcrypt_pbkdf'
label = 'C/Java ext'
else
require 'bcrypt_pbkdf/pure_ruby'
label = 'Pure Ruby '
end
vectors = [
["pass2", "salt2", 12, 2],
["password", "salt", 32, 4],
["password", "salt", 64, 8],
["password", "salt", 16, 42],
["password", "salt", 32, 16],
]
WARMUP = native ? 5 : 1
N = native ? 20 : 2
puts "#{label} — #{RUBY_DESCRIPTION}"
puts "%-30s %8s" % ["input (keylen/rounds)", "ms/call"]
puts "-" * 42
vectors.each do |pass, salt, keylen, rounds|
WARMUP.times { BCryptPbkdf::Engine.__bc_crypt_pbkdf(pass, salt, keylen, rounds) }
t = Benchmark.realtime { N.times { BCryptPbkdf::Engine.__bc_crypt_pbkdf(pass, salt, keylen, rounds) } }
puts "%-30s %8.1f" % ["#{pass[0,8]}/#{keylen}/#{rounds}", t / N * 1000]
end# MRI — C ext
bundle exec ruby bench/pure_ruby_bench.rb
# MRI — pure Ruby
NATIVE=0 bundle exec ruby bench/pure_ruby_bench.rb
# JRuby — Java ext (build jar first: bundle exec rake compile)
jruby -Ilib bench/pure_ruby_bench.rb
# JRuby — pure Ruby fallback
NATIVE=0 jruby -Ilib bench/pure_ruby_bench.rb |
When the C extension is unavailable (JRuby, or any environment without a native compiler), load a pure Ruby implementation instead of failing with a
LoadError.The pure Ruby path is ~68x slower than the C extension at
rounds=4(~930ms vs ~14ms), butbcrypt_pbkdfis only used in net-ssh for decrypting encrypted Ed25519 private keys — a one-time cost at connection time — so this is acceptable.JRuby users currently hit a hardcoded
raise "BCryptPbkdf is not implemented for jruby"in net-ssh; with this change that can be removed.All existing test vectors pass under the pure Ruby implementation.