From db336854756e7d4b7fbae6ddc3f8f675718d8c55 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B4=AE=E7=94=9F=EF=BC=88=E5=AD=90=E8=99=9A=EF=BC=89?= <2234839456@qq.com> Date: Sat, 25 Jul 2026 02:00:05 +0800 Subject: [PATCH] =?UTF-8?q?perf(gsub):=20serializeSingleSubst=20=E5=A4=A7?= =?UTF-8?q?=20coverage=20=E5=8F=8D=E8=BD=AC=E9=81=8D=E5=8E=86=EF=BC=8C?= =?UTF-8?q?=E6=80=9D=E6=BA=90=20-63%?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 思源 GSUB 56 lookup 中 4 个非空 type1 lookup(locl 地区变体)的 coverage 极大(lookup#43 = 8881 gid,#0=729,#42=636),但子集仅命中个位数。 原 serializeSingleSubst 用 readCoverageGids 全量展开 coverage 再逐个查 gidLookup(8881 次几乎全未命中),占 subsetGSUB 84%(0.184ms/call)。 改为遍历子集原始 gid(currentSortedSubsetGids,仅 19 项)+ coverageIndexOf 在 coverage 中二分定位下标(format2 下标对应 substituteGlyphIDs 数组), O(subsetSize × log covCount) 替代 O(covCount)。与 reachable 侧 [[gsub-reachable-bigcov-byte-bsearch]] 同一类「展开全量消费极少」浪费。 仅当 covGidCount > subsetSize×4 且 >16 时启用反转,短 coverage(FiraCode 等)走原路径避免二分开销。导出 coverageIndexOf/coverageCount 供复用。 subsetGSUB 思源 min 0.204→0.075ms(-63%),otf-思源 woff2 avg 2.7→1.0ms。 输出逐字节一致(思源 GSUB 6560 bytes 差异 0),全字体 SSIM 不变。 Co-Authored-By: Claude Opus 4.8 (1M context) --- backend/font_util/gsub-reachable.ts | 4 +- backend/font_util/gsub-subset.ts | 67 ++++++++++++++++++++++------- 2 files changed, 54 insertions(+), 17 deletions(-) diff --git a/backend/font_util/gsub-reachable.ts b/backend/font_util/gsub-reachable.ts index 7288795..a2b1ef3 100644 --- a/backend/font_util/gsub-reachable.ts +++ b/backend/font_util/gsub-reachable.ts @@ -145,7 +145,7 @@ function coverageFirstExcludedGid( * @param gid 待查 gid * @returns gid 在 coverage 中的序号;不在返回 -1 */ -function coverageIndexOf(r: OTReader, off: number, gid: number): number { +export function coverageIndexOf(r: OTReader, off: number, gid: number): number { const dv = r.dv; const format = dv.getUint16(off, false); if (format === 1) { @@ -195,7 +195,7 @@ function coverageIndexOf(r: OTReader, off: number, gid: number): number { * 反转遍历路径判断「coverage 是否明显多于 reachable」只需 gid 数,无需展开(思源 locl coverage * 达上万 gid,展开即 #1 热点)。format1 直接读 count;format2 累加各 range 的 (end-start+1)。 */ -function coverageCount(r: OTReader, off: number): number { +export function coverageCount(r: OTReader, off: number): number { const dv = r.dv; const format = dv.getUint16(off, false); if (format === 1) { diff --git a/backend/font_util/gsub-subset.ts b/backend/font_util/gsub-subset.ts index 5339423..cfc3edd 100644 --- a/backend/font_util/gsub-subset.ts +++ b/backend/font_util/gsub-subset.ts @@ -22,6 +22,7 @@ */ import { OTWriter as Writer, OTReader as Reader, serializeScriptList, serializeFeatureList, scriptListSpan, featureListSpan } from "./ot-bytes.js"; +import { coverageIndexOf, coverageCount } from "./gsub-reachable.js"; /** GSUB lookup 类型常量 */ const LT_SINGLE = 1; @@ -371,26 +372,62 @@ function serializeSingleSubst( const dv = r.dv; const format = r.u16(off); const covOff = off + r.u16(off + 2); - const covGids = readCoverageGids(r, covOff); /** 收集 (from新gid → to新gid) 有效项,子集外的剔除 */ const entries: Array<{ from: number; to: number }> = []; - if (format === 1) { - const delta = r.i16(off + 4); - for (const g of covGids) { - const fromNew = gidLookup[g]; - const toNew = gidLookup[(g + delta) & 0xffff]; - if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew }); - } - } else if (format === 2) { - const count = r.u16(off + 4); - for (let i = 0; i < covGids.length && i < count; i++) { - const fromNew = gidLookup[covGids[i]]; - const toNew = gidLookup[dv.getUint16(off + 6 + i * 2, false)]; - if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew }); + + /** 反转遍历快路径:思源 locl 等 type1 lookup 的 coverage 可达上千~上万 gid(lookup#43 = 8881), + * 但子集仅命中个位数。原实现 readCoverageGids 全量展开 coverage 再逐个查 gidLookup(8881 次, + * 几乎全未命中)。改为遍历子集原始 gid(currentSortedSubsetGids,仅 19 项),用 coverageIndexOf + * 在 coverage 中二分定位下标,命中才取 target——O(subsetSize × log covCount) 替代 O(covCount)。 + * 与 [[gsub-reachable-type1-fmt2-reverse-iter]] 同思路。仅当 coverage 明显多于子集时启用, + * 否则短 coverage 原遍历更快(二分开销 > 跳过收益)。 */ + const subsetGids = currentSortedSubsetGids; + const covGidCount = coverageCount(r, covOff); + /** 阈值:coverage gid 数 / 子集 gid 数 > 4 且 coverage 较大时启用反转 */ + const useReverse = covGidCount > subsetGids.length * 4 && covGidCount > 16; + + if (useReverse) { + if (format === 1) { + const delta = r.i16(off + 4); + for (const g of subsetGids) { + /** from gid 必须在 coverage 中(SingleSubst 仅对 coverage 内 gid 生效) */ + if (coverageIndexOf(r, covOff, g) < 0) continue; + const fromNew = gidLookup[g]; + const toNew = gidLookup[(g + delta) & 0xffff]; + if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew }); + } + } else if (format === 2) { + for (const g of subsetGids) { + /** idx = gid 在 coverage 中的序号,与 substituteGlyphIDs 数组下标一一对应 */ + const idx = coverageIndexOf(r, covOff, g); + if (idx < 0) continue; + const fromNew = gidLookup[g]; + const toNew = gidLookup[dv.getUint16(off + 6 + idx * 2, false)]; + if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew }); + } + } else { + return false; } } else { - return false; + const covGids = readCoverageGids(r, covOff); + if (format === 1) { + const delta = r.i16(off + 4); + for (const g of covGids) { + const fromNew = gidLookup[g]; + const toNew = gidLookup[(g + delta) & 0xffff]; + if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew }); + } + } else if (format === 2) { + const count = r.u16(off + 4); + for (let i = 0; i < covGids.length && i < count; i++) { + const fromNew = gidLookup[covGids[i]]; + const toNew = gidLookup[dv.getUint16(off + 6 + i * 2, false)]; + if (fromNew >= 0 && toNew >= 0) entries.push({ from: fromNew, to: toNew }); + } + } else { + return false; + } } if (entries.length === 0) return false;