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:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7282589689a627d93346959e47ca2c5dcd7f37c496e61ab0e58e5bd794082010
|
|
4
|
+
data.tar.gz: 3b9b78d8fbb79c59b2603268955b9a92ee9d5046495b778ae1b1ec9f1b60fe51
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
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
|
-
|
|
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
|
-
|
|
73
|
-
|
|
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 =
|
|
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
|
-
#
|
|
343
|
-
|
|
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
|
data/lib/omnizip/version.rb
CHANGED
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.
|
|
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-
|
|
11
|
+
date: 2026-08-26 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: base64
|