omnizip 0.3.23 → 0.3.25

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: 61cf26f9e324600f286b236ab80e02d720059f32bca2fa3c71640ba3abc4355a
4
- data.tar.gz: fa189e081e402ecb4d084490e7874402faacf231a296cd009a15f218fcd9453e
3
+ metadata.gz: 7282589689a627d93346959e47ca2c5dcd7f37c496e61ab0e58e5bd794082010
4
+ data.tar.gz: 3b9b78d8fbb79c59b2603268955b9a92ee9d5046495b778ae1b1ec9f1b60fe51
5
5
  SHA512:
6
- metadata.gz: 308660d6095a909d72c528bdfb5e828e76d320172b979a53ef7401aa2c1c12a560a0afd57f49991eb5190f87deca1c15cd798e8abae89bd3341ed3bb43c39b3d
7
- data.tar.gz: 29ac9066539f2d8cef3973a2eb9cfe15a549a5efead04a1f5c88f28587fb6a2d23ac28c401c1c4187f5c226bfe4b531e3565523400dcd183adc83c842cd2d5dc
6
+ metadata.gz: 5c9ccbbc2d410b760251c708499720b4c0ee598fc1b9307d637d42b5cd88f1744f4123fd12b18c05561b7b2eb396f22a8054435e6a19af7ef9cbd181a7e23ca7
7
+ data.tar.gz: 419850a99a4fccbedc21cba3df34b89ebfaf0a1949bcfe7cf93544b55162ff118dcfa71053a64833d608f95a514ec8054a6b23f2df7d464426302ebea1994b04
data/CHANGELOG.md CHANGED
@@ -7,6 +7,27 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.3.25] - 2026-08-26
11
+
12
+ ### Changed
13
+ - BZip2 Burrows-Wheeler Transform: rotation comparator compares 4
14
+ big-endian bytes per step (Fixnum-safe, no modulo) over a doubled
15
+ byte array. 21x fewer allocations per 138 KB compressed, with
16
+ byte-identical output and 25 case-round-tripped correctness
17
+ (random and degenerate). The BZip2 codec itself does not emit a
18
+ standard bzip2 file format (pre-existing, separate issue).
19
+
20
+ ## [0.3.24] - 2026-08-26
21
+
22
+ ### Changed
23
+ - LZMA2 encoder hot paths avoid per-call allocations: the
24
+ range-encoder symbol drain reuses its 10 KB scratch buffer
25
+ (profiles showed String#* at ~half of encoder CPU), and the optimal
26
+ parser compares 8-byte windows and selects best matches without
27
+ intermediate strings/arrays. ~180k fewer object allocations and
28
+ ~94 MB less churn per 138 KB compressed, with byte-identical
29
+ output; covers both the xz and 7-Zip paths.
30
+
10
31
  ## [0.3.23] - 2026-08-26
11
32
 
12
33
  ### Changed
@@ -49,13 +49,16 @@ module Omnizip
49
49
  n = data.length
50
50
  bytes = data.bytes
51
51
 
52
+ # Doubled bytes: rotation a's byte at offset o is
53
+ # dbl[a + o] — indexing needs no modulo, and comparisons
54
+ # can run 8 bytes at a time (modulo arithmetic in the
55
+ # comparator dominated the encoder's profile).
56
+ dbl = bytes * 2
57
+
52
58
  # Build suffix array without creating rotation strings
53
- # Use direct byte comparison for efficiency
54
59
  suffix_array = (0...n).to_a
55
-
56
- # Sort using optimized comparison that avoids string allocation
57
60
  suffix_array.sort! do |a, b|
58
- compare_rotations(bytes, a, b, n)
61
+ compare_rotations(dbl, a, b, n)
59
62
  end
60
63
 
61
64
  # Find primary index (position where suffix starts at 0)
@@ -63,7 +66,7 @@ module Omnizip
63
66
 
64
67
  # Extract last column (character before each suffix)
65
68
  transformed = suffix_array.map do |idx|
66
- bytes[(idx - 1) % n]
69
+ dbl[n + idx - 1]
67
70
  end.pack("C*").b
68
71
 
69
72
  [transformed, primary_index]
@@ -149,23 +152,32 @@ module Omnizip
149
152
  # @param b [Integer] Second rotation start index
150
153
  # @param n [Integer] Length
151
154
  # @return [Integer] -1, 0, or 1 for comparison result
152
- def compare_rotations(bytes, a, b, n)
153
- # Fast path: compare first few bytes directly
154
- 8.times do |offset|
155
- byte_a = bytes[(a + offset) % n]
156
- byte_b = bytes[(b + offset) % n]
157
- cmp = byte_a <=> byte_b
158
- return cmp if cmp != 0
155
+ # Compare rotations a and b over the doubled byte array.
156
+ # Four big-endian bytes per step: the chunk fits a Fixnum (no
157
+ # Bignum allocation), the low-offset byte dominates so the
158
+ # order is lexicographic, and the doubled array removes the
159
+ # per-byte modulo.
160
+ def compare_rotations(dbl, a, b, n)
161
+ len = 0
162
+ while len + 4 <= n
163
+ wa = (dbl[a + len] << 24) |
164
+ (dbl[a + len + 1] << 16) |
165
+ (dbl[a + len + 2] << 8) |
166
+ dbl[a + len + 3]
167
+ wb = (dbl[b + len] << 24) |
168
+ (dbl[b + len + 1] << 16) |
169
+ (dbl[b + len + 2] << 8) |
170
+ dbl[b + len + 3]
171
+ return wa <=> wb if wa != wb
172
+
173
+ len += 4
159
174
  end
160
-
161
- # Continue comparing remaining bytes
162
- (8...n).each do |offset|
163
- byte_a = bytes[(a + offset) % n]
164
- byte_b = bytes[(b + offset) % n]
165
- cmp = byte_a <=> byte_b
175
+ while len < n
176
+ cmp = dbl[a + len] <=> dbl[b + len]
166
177
  return cmp if cmp != 0
167
- end
168
178
 
179
+ len += 1
180
+ end
169
181
  0
170
182
  end
171
183
 
@@ -67,11 +67,12 @@ module Omnizip
67
67
  ].min
68
68
 
69
69
  len = 2
70
- # Compare 8 bytes at a time using 64-bit integers
70
+ # Compare 8 bytes at a time using 64-bit integers read
71
+ # without intermediate strings (byteslice + unpack1
72
+ # allocates two Strings per step and dominated GC time).
71
73
  while len + 8 <= max_match_len
72
- # Read 8 bytes as little-endian 64-bit integers
73
- v1 = buf.byteslice(buf_pos + len, 8).unpack1("Q<")
74
- v2 = buf.byteslice(buf_back_index + len, 8).unpack1("Q<")
74
+ v1 = read8(buf, buf_pos + len)
75
+ v2 = read8(buf, buf_back_index + len)
75
76
  break if v1 != v2
76
77
 
77
78
  len += 8
@@ -104,7 +105,15 @@ module Omnizip
104
105
  # A match may reference anything before `position`: bytes already
105
106
  # committed to the dictionary *and* bytes decoded earlier within
106
107
  # the chunk currently being encoded.
107
- best_normal = matches.max_by { |m| [m.length, m.distance] }
108
+ best_normal = nil
109
+ matches.each do |m|
110
+ next if best_normal &&
111
+ (m.length < best_normal.length ||
112
+ (m.length == best_normal.length &&
113
+ m.distance <= best_normal.distance))
114
+
115
+ best_normal = m
116
+ end
108
117
 
109
118
  if best_normal && best_normal.length >= nice_len &&
110
119
  best_normal.distance <= position
@@ -126,6 +135,18 @@ module Omnizip
126
135
  end
127
136
  end
128
137
 
138
+ # Allocation-free little-endian 8-byte read.
139
+ def read8(buf, pos)
140
+ buf.getbyte(pos) |
141
+ (buf.getbyte(pos + 1) << 8) |
142
+ (buf.getbyte(pos + 2) << 16) |
143
+ (buf.getbyte(pos + 3) << 24) |
144
+ (buf.getbyte(pos + 4) << 32) |
145
+ (buf.getbyte(pos + 5) << 40) |
146
+ (buf.getbyte(pos + 6) << 48) |
147
+ (buf.getbyte(pos + 7) << 56)
148
+ end
149
+
129
150
  # Constants
130
151
  REPS = 4
131
152
  MATCH_LEN_MAX = 273 # From lzma.h
@@ -339,8 +339,11 @@ module Omnizip
339
339
  def encode_queued_symbols(encoder, output)
340
340
  return if encoder.none?
341
341
 
342
- # Create temporary buffer for encoding
343
- temp_buffer = "\0" * 10000
342
+ # Reused scratch buffer: allocating a fresh 10 KB String
343
+ # per drain showed up as half the encoder's runtime via
344
+ # GC (String#* in profiles).
345
+ @symbol_buffer ||= "\0".b * 10_000
346
+ temp_buffer = @symbol_buffer
344
347
  out_pos = Omnizip::Algorithms::LZMA::IntRef.new(0)
345
348
 
346
349
  # Track size before encoding
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Omnizip
4
- VERSION = "0.3.23"
4
+ VERSION = "0.3.25"
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: omnizip
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.23
4
+ version: 0.3.25
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ribose Inc.
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2026-08-25 00:00:00.000000000 Z
11
+ date: 2026-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: base64