streaming 7-Zip archive extractor
0

Configure Feed

Select the types of activity you want to include in your feed.

refactor: fuse isMatch bit and literal decode for the hot path

the dominant decode step for literal-heavy payloads is "a plain literal
follows a literal" (state < LIT_STATES). previously this ran as two
RangeDecoder method calls — decodeBit(isMatch) then decodeLiteralSymbol —
each loading this.range/this.code at entry and storing them at exit, so
the range-coder state round-tripped through fields between them.

add decodeIsMatchThenLiteralSymbol, which decodes the isMatch bit and (if
it is a literal) flows straight into the branchless literal loop with
range/code held in locals throughout. decode() takes this fused path only
for state < LIT_STATES and inlines the stateAfterLiteral transition there;
the match-byte literal path (state >= LIT_STATES) and all match/rep
decoding are unchanged.

measured on the manga .7z corpus: aggregate ~21.1 -> ~22.4 MiB/s (+6%),
roughly +27% over the pre-optimization baseline. no new hot-path deopts.

+88 -3
+88 -3
lib/codec/lzma.ts
··· 310 310 } 311 311 312 312 /** 313 + * fused `isMatch`-bit + plain-literal decode for the dominant decode step 314 + * (literal-heavy payloads in the `state < LIT_STATES` case). decoding both 315 + * in one method keeps `range`/`code` in locals across the boundary instead 316 + * of round-tripping them through `this.range`/`this.code` between a separate 317 + * {@link decodeBit} and {@link decodeLiteralSymbol} call. 318 + * 319 + * @param isMatch the isMatch probability table 320 + * @param isMatchIdx index of the isMatch bit for the current state+posState 321 + * @param probs the literal probability table 322 + * @param offset starting index of the literal sub-coder in `probs` 323 + * @returns the decoded literal byte (0–255), or -1 when `isMatch` decoded as 324 + * 1 (a match) — in which case the caller takes the match path 325 + */ 326 + decodeIsMatchThenLiteralSymbol( 327 + isMatch: Uint16Array, 328 + isMatchIdx: number, 329 + probs: Uint16Array, 330 + offset: number, 331 + ): number { 332 + let range = this.range; 333 + let code = this.code; 334 + // isMatch bit — branchy, like decodeBit (skewed, predicts well). 335 + if (range < TOP_VALUE) { 336 + const b = this.#pos < this.#end ? this.#buf[this.#pos++] : 0; 337 + code = ((code << SHIFT_BITS) | b) >>> 0; 338 + range = (range << SHIFT_BITS) >>> 0; 339 + } 340 + { 341 + const prob = isMatch[isMatchIdx]; 342 + const bound = ((range >>> BIT_MODEL_TOTAL_BITS) * prob) >>> 0; 343 + if (code >= bound) { 344 + this.range = (range - bound) >>> 0; 345 + this.code = (code - bound) >>> 0; 346 + isMatch[isMatchIdx] = prob - (prob >>> MOVE_BITS); 347 + return -1; 348 + } 349 + range = bound; 350 + isMatch[isMatchIdx] = prob + ((BIT_MODEL_TOTAL - prob) >>> MOVE_BITS); 351 + } 352 + // literal symbol — branchless, identical to decodeLiteralSymbol. 353 + let symbol = 1; 354 + while (symbol < 0x100) { 355 + if (range < TOP_VALUE) { 356 + const b = this.#pos < this.#end ? this.#buf[this.#pos++] : 0; 357 + code = ((code << SHIFT_BITS) | b) >>> 0; 358 + range = (range << SHIFT_BITS) >>> 0; 359 + } 360 + const idx = offset + symbol; 361 + const prob = probs[idx]; 362 + const bound = ((range >>> BIT_MODEL_TOTAL_BITS) * prob) >>> 0; 363 + const bit = code < bound ? 0 : 1; 364 + range = (bit === 0 ? bound : range - bound) >>> 0; 365 + code = (bit === 0 ? code : code - bound) >>> 0; 366 + probs[idx] = bit === 0 ? prob + ((BIT_MODEL_TOTAL - prob) >>> MOVE_BITS) : prob - (prob >>> MOVE_BITS); 367 + symbol = (symbol << 1) | bit; 368 + } 369 + this.range = range; 370 + this.code = code; 371 + return symbol & 0xff; 372 + } 373 + 374 + /** 313 375 * decodes one 8-bit literal symbol using the match-byte path — used when 314 376 * the previous LZ window byte at the most-recent rep distance influences 315 377 * the prob selection. same idea as {@link decodeLiteralSymbol}. ··· 706 768 return false; 707 769 } 708 770 const posState = lz.position & this.#posMask; 709 - const stateIdx = this.#state; 710 - if (rc.decodeBit(this.#isMatch, stateIdx * POS_STATES_MAX + posState) === 0) { 771 + const state = this.#state; 772 + if (state < LIT_STATES) { 773 + // dominant path for literal-heavy data: a plain literal follows a 774 + // literal. fuse the isMatch bit and the literal-symbol decode so 775 + // the range coder's locals never round-trip through fields. 776 + const prev = lz.getByte(0); 777 + const offset = 778 + (((prev >>> (8 - this.#lc)) + ((lz.position & this.#literalPosMask) << this.#lc)) >>> 0) * 0x300; 779 + const symbol = rc.decodeIsMatchThenLiteralSymbol( 780 + this.#isMatch, 781 + state * POS_STATES_MAX + posState, 782 + this.#literalProbs, 783 + offset, 784 + ); 785 + if (symbol >= 0) { 786 + lz.putByte(symbol); 787 + // stateAfterLiteral inlined for state < LIT_STATES: states 0–3 788 + // collapse to LitLit, states 4–6 drop by 3. 789 + this.#state = state <= St.ShortrepLitLit ? St.LitLit : state - 3; 790 + continue; 791 + } 792 + // isMatch decoded as 1 — fall through to the match path. 793 + } else if (rc.decodeBit(this.#isMatch, state * POS_STATES_MAX + posState) === 0) { 711 794 this.#decodeLiteral(lz, rc); 712 - } else { 795 + continue; 796 + } 797 + { 713 798 let len: number; 714 799 if (rc.decodeBit(this.#isRep, this.#state) === 0) { 715 800 len = this.#decodeMatch(posState, rc);