djb2 0.1.1 → 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 69efd4b5520aefff0c661214b57a87aa95a3eb64baa0fa298bc6a4a282fea476
4
- data.tar.gz: 848fa3e0e64c9fcc8d595232c4507478b0ed530bfa7d751a85d3dad5f68f0dc4
3
+ metadata.gz: 2daeb6437c508af570b7efcab866f08594ca051704f4d1da3b43e4a0da2a47c3
4
+ data.tar.gz: b4964f45412483fd9ed4d8d77f4f74f92fa361efd174a650c98386270ef60363
5
5
  SHA512:
6
- metadata.gz: b08618ceb92187f72f6e6bc743ae199e332a86c62d6075e2fee824641565ea815c6121af9fa4d8ecb7ce2750363d670147b380880734f3edf0fcf8796c9ea13a
7
- data.tar.gz: 4988363fafdd18547b51d683b94eca713a828d523e1a30550bb6a62e8aa1cba23d9d26c040fab4daed2263c13d793a330eb2b63da7143f3c6738c9e22d52f96d
6
+ metadata.gz: 2460212ef211b32a57c3e1885a8c7d8f57319db9fbcb0430529d35e6a85a54769f3449d98d16073e960ae07413b8ec9ea8b05a2a785c15e78d241fd07024c2f4
7
+ data.tar.gz: 8785a9c2dcdb3bdacf3285d1c0a0f0f5fc8f9c6b430f28276f4543ee3aab8e6546e5906b137a7fa8ee1c0865860c2c3b986517ece8b076e2c1462a0b1e6f7b8c
data/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # DJB2
2
2
 
3
- Native djb2 hash implementation
3
+ Pure Ruby djb2 hash implementation, optimized for YJIT.
4
4
 
5
5
  ## Installation
6
6
 
data/Rakefile CHANGED
@@ -9,14 +9,4 @@ Rake::TestTask.new(:test) do |t|
9
9
  t.test_files = FileList["test/**/test_*.rb"]
10
10
  end
11
11
 
12
- require "rake/extensiontask"
13
-
14
- task build: :compile
15
-
16
- GEMSPEC = Gem::Specification.load("djb2.gemspec")
17
-
18
- Rake::ExtensionTask.new("djb2", GEMSPEC) do |ext|
19
- ext.lib_dir = "lib/djb2"
20
- end
21
-
22
- task default: %i[clobber compile test]
12
+ task default: :test
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Re-exec with --yjit if YJIT is available but not yet enabled.
4
+ if defined?(RubyVM::YJIT) && !RubyVM::YJIT.enabled?
5
+ exec(RbConfig.ruby, "--yjit", $0, *ARGV)
6
+ end
7
+
8
+ require "bundler/inline"
9
+
10
+ gemfile do
11
+ source "https://rubygems.org"
12
+ gem "benchmark-ips"
13
+ gem "djb2", "0.1.1"
14
+ end
15
+
16
+ # ── Load the C extension (released gem) ──────────────────────────────────────
17
+ require "djb2"
18
+ c_digest = DJB2.method(:digest)
19
+
20
+ # ── Pure Ruby implementation (this branch) ───────────────────────────────────
21
+ load File.expand_path("../lib/djb2.rb", __dir__)
22
+ ruby_digest = DJB2.method(:digest)
23
+
24
+ # ── Verify correctness ──────────────────────────────────────────────────────
25
+ test_strings = ["foo", "bar", "hello world", "x" * 100, Random.bytes(1000)]
26
+ test_strings.each do |s|
27
+ c_result = c_digest.call(s)
28
+ ruby_result = ruby_digest.call(s)
29
+ unless c_result == ruby_result
30
+ abort "MISMATCH on #{s.inspect[0..40]}: C=#{c_result} Ruby=#{ruby_result}"
31
+ end
32
+ end
33
+ puts "Correctness verified: both implementations produce identical results."
34
+ puts
35
+
36
+ # ── Environment ──────────────────────────────────────────────────────────────
37
+ puts "Ruby: #{RUBY_VERSION} (#{RUBY_PLATFORM})"
38
+ puts "YJIT: #{RubyVM::YJIT.enabled? ? "enabled" : "DISABLED"}"
39
+ puts
40
+
41
+ # ── Warmup YJIT ─────────────────────────────────────────────────────────────
42
+ warmup_str = "warmup" * 50
43
+ 20_000.times { c_digest.call(warmup_str) }
44
+ 20_000.times { ruby_digest.call(warmup_str) }
45
+ puts "YJIT warmup complete (20k iterations each)."
46
+ puts
47
+
48
+ # ── Benchmark ────────────────────────────────────────────────────────────────
49
+ inputs = {
50
+ "short (5B)" => "hello",
51
+ "realistic (134B)" => "{\"template_name\":\"customers\\/account.json\",\"section_id\":\"section-id\",\"block_id\":\"abc\\/\\/dc\\u0026f\",\"setting_id\":\"setting11111-id\"}",
52
+ "medium (100B)" => "x" * 100,
53
+ "long (1KB)" => "y" * 1024,
54
+ "long (10KB)" => "z" * 10240,
55
+ }
56
+
57
+ inputs.each do |label, str|
58
+ puts "=" * 60
59
+ puts " #{label} (#{str.bytesize} bytes)"
60
+ puts "=" * 60
61
+
62
+ Benchmark.ips do |x|
63
+ x.config(warmup: 2, time: 5)
64
+
65
+ x.report("C extension") { c_digest.call(str) }
66
+ x.report("Pure Ruby") { ruby_digest.call(str) }
67
+
68
+ x.compare!
69
+ end
70
+ puts
71
+ end
data/lib/djb2/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module DJB2
4
- VERSION = "0.1.1"
4
+ VERSION = "0.2.0"
5
5
  end
data/lib/djb2.rb CHANGED
@@ -2,16 +2,66 @@
2
2
 
3
3
  require_relative "djb2/version"
4
4
 
5
- begin
6
- # Load the precompiled version of the library
7
- ruby_version = /(\d+\.\d+)/.match(RUBY_VERSION)
8
- require "djb2/#{ruby_version}/djb2"
9
- rescue LoadError
10
- # It's important to leave for users that can not or don't want to use the gem with precompiled binaries.
11
- require "djb2/djb2"
12
- end
13
-
14
5
  module DJB2
15
6
  class Error < StandardError; end
16
- # Your code goes here...
7
+
8
+ # Computes the djb2 hash (xor variant) of the given string.
9
+ #
10
+ # The hash is computed using 64-bit arithmetic split into two 32-bit
11
+ # halves to keep all intermediate values within Ruby's Fixnum range,
12
+ # avoiding Bignum allocation in the hot loop. This makes the
13
+ # implementation YJIT-friendly: the JIT can emit efficient native
14
+ # code for the entire loop without any object allocations.
15
+ #
16
+ # @param string [String] the string to hash (binary-safe, operates on raw bytes)
17
+ # @return [Integer] a 64-bit unsigned integer hash value
18
+ # @raise [TypeError] if the argument is not a String
19
+ def self.digest(string)
20
+ raise TypeError, "no implicit conversion of #{string.class} into String" unless string.is_a?(String)
21
+
22
+ hi = 0 # upper 32 bits of the hash
23
+ lo = 5381 # lower 32 bits of the hash
24
+ i = 0
25
+ len = string.bytesize
26
+
27
+ # Process 4 bytes at a time to reduce loop overhead.
28
+ stop = len - (len & 3)
29
+ while i < stop
30
+ # Multiply (hi:lo) by 33 using: x * 33 = (x << 5) + x
31
+ # lo half: must mask (lo << 5) to 32 bits BEFORE adding lo, so that
32
+ # the carry into the hi half is correct (0 or 1, never more).
33
+ t = ((lo << 5) & 0xFFFFFFFF) + lo
34
+ # hi half: (hi << 5) can be up to 37 bits, but the total expression
35
+ # still fits in a Fixnum (< 62 bits), so we only mask at the end.
36
+ hi = ((hi << 5) + (lo >> 27) + hi + (t >> 32)) & 0xFFFFFFFF
37
+ # XOR the current byte into the lo half.
38
+ lo = (t & 0xFFFFFFFF) ^ string.getbyte(i)
39
+
40
+ t = ((lo << 5) & 0xFFFFFFFF) + lo
41
+ hi = ((hi << 5) + (lo >> 27) + hi + (t >> 32)) & 0xFFFFFFFF
42
+ lo = (t & 0xFFFFFFFF) ^ string.getbyte(i + 1)
43
+
44
+ t = ((lo << 5) & 0xFFFFFFFF) + lo
45
+ hi = ((hi << 5) + (lo >> 27) + hi + (t >> 32)) & 0xFFFFFFFF
46
+ lo = (t & 0xFFFFFFFF) ^ string.getbyte(i + 2)
47
+
48
+ t = ((lo << 5) & 0xFFFFFFFF) + lo
49
+ hi = ((hi << 5) + (lo >> 27) + hi + (t >> 32)) & 0xFFFFFFFF
50
+ lo = (t & 0xFFFFFFFF) ^ string.getbyte(i + 3)
51
+
52
+ i += 4
53
+ end
54
+
55
+ # Handle remaining 0-3 bytes.
56
+ while i < len
57
+ t = ((lo << 5) & 0xFFFFFFFF) + lo
58
+ hi = ((hi << 5) + (lo >> 27) + hi + (t >> 32)) & 0xFFFFFFFF
59
+ lo = (t & 0xFFFFFFFF) ^ string.getbyte(i)
60
+ i += 1
61
+ end
62
+
63
+ # Combine the two halves into a 64-bit result. Using multiply instead
64
+ # of (hi << 32) | lo avoids a YJIT side exit caused by left shift overflow.
65
+ hi * 0x100000000 + lo
66
+ end
17
67
  end
metadata CHANGED
@@ -1,21 +1,18 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: djb2
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.1
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2026-02-19 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies: []
13
- description:
14
12
  email:
15
13
  - jean.boussier@gmail.com
16
14
  executables: []
17
- extensions:
18
- - ext/djb2/extconf.rb
15
+ extensions: []
19
16
  extra_rdoc_files: []
20
17
  files:
21
18
  - ".ruby-version"
@@ -23,8 +20,7 @@ files:
23
20
  - LICENSE.txt
24
21
  - README.md
25
22
  - Rakefile
26
- - ext/djb2/djb2.c
27
- - ext/djb2/extconf.rb
23
+ - benchmark/benchmark.rb
28
24
  - lib/djb2.rb
29
25
  - lib/djb2/version.rb
30
26
  homepage: https://github.com/Shopify/djb2
@@ -34,7 +30,6 @@ metadata:
34
30
  bug_tracker_uri: https://github.com/Shopify/djb2/issues
35
31
  source_code_uri: https://github.com/Shopify/djb2
36
32
  allowed_push_host: https://rubygems.org
37
- post_install_message:
38
33
  rdoc_options: []
39
34
  require_paths:
40
35
  - lib
@@ -42,15 +37,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
42
37
  requirements:
43
38
  - - ">="
44
39
  - !ruby/object:Gem::Version
45
- version: 2.6.0
40
+ version: 3.2.0
46
41
  required_rubygems_version: !ruby/object:Gem::Requirement
47
42
  requirements:
48
43
  - - ">="
49
44
  - !ruby/object:Gem::Version
50
45
  version: '0'
51
46
  requirements: []
52
- rubygems_version: 3.3.27
53
- signing_key:
47
+ rubygems_version: 4.0.3
54
48
  specification_version: 4
55
- summary: Native djb2 hash implementation
49
+ summary: Pure Ruby djb2 hash implementation
56
50
  test_files: []
data/ext/djb2/djb2.c DELETED
@@ -1,28 +0,0 @@
1
- #include "ruby.h"
2
-
3
- VALUE rb_mDJB2;
4
-
5
- uint64_t
6
- djb_hash_2(const char *string, long length)
7
- {
8
- uint64_t hash = 5381;
9
- long index;
10
- for (index = 0; index < length; index++) {
11
- hash = (hash * 33) ^ (unsigned char)string[index];
12
- }
13
- return hash;
14
- }
15
-
16
- static VALUE
17
- rb_djb_hash_2(VALUE self, VALUE str)
18
- {
19
- Check_Type(str, T_STRING);
20
- return ULL2NUM(djb_hash_2(RSTRING_PTR(str), RSTRING_LEN(str)));
21
- }
22
-
23
- RUBY_FUNC_EXPORTED void
24
- Init_djb2(void)
25
- {
26
- rb_mDJB2 = rb_define_module("DJB2");
27
- rb_define_singleton_method(rb_mDJB2, "digest", rb_djb_hash_2, 1);
28
- }
data/ext/djb2/extconf.rb DELETED
@@ -1,10 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "mkmf"
4
-
5
- # Makes all symbols private by default to avoid unintended conflict
6
- # with other gems. To explicitly export symbols you can use RUBY_FUNC_EXPORTED
7
- # selectively, or entirely remove this flag.
8
- append_cflags("-fvisibility=hidden")
9
-
10
- create_makefile("djb2/djb2")