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 +4 -4
- data/README.md +1 -1
- data/Rakefile +1 -11
- data/benchmark/benchmark.rb +71 -0
- data/lib/djb2/version.rb +1 -1
- data/lib/djb2.rb +60 -10
- metadata +7 -13
- data/ext/djb2/djb2.c +0 -28
- data/ext/djb2/extconf.rb +0 -10
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 2daeb6437c508af570b7efcab866f08594ca051704f4d1da3b43e4a0da2a47c3
|
|
4
|
+
data.tar.gz: b4964f45412483fd9ed4d8d77f4f74f92fa361efd174a650c98386270ef60363
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 2460212ef211b32a57c3e1885a8c7d8f57319db9fbcb0430529d35e6a85a54769f3449d98d16073e960ae07413b8ec9ea8b05a2a785c15e78d241fd07024c2f4
|
|
7
|
+
data.tar.gz: 8785a9c2dcdb3bdacf3285d1c0a0f0f5fc8f9c6b430f28276f4543ee3aab8e6546e5906b137a7fa8ee1c0865860c2c3b986517ece8b076e2c1462a0b1e6f7b8c
|
data/README.md
CHANGED
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
|
-
|
|
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
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
|
-
|
|
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.
|
|
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:
|
|
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
|
-
-
|
|
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.
|
|
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:
|
|
53
|
-
signing_key:
|
|
47
|
+
rubygems_version: 4.0.3
|
|
54
48
|
specification_version: 4
|
|
55
|
-
summary:
|
|
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")
|