Skip to content

Commit 6750bb6

Browse files
perf(iconv): DBCS encode direct tables — Big5 1.7x, all 7 encoders O(1)
The 2-byte DBCS encoders (Shift-JIS, Big5, GBK, EUC-KR, CP949, GB2312, Johab) all binary-searched their sorted (cp, packed) table per char (thousands of entries: Big5 14030, EUC-KR/etc), running ~3x SLOWER than glibc on UTF-8 -> DBCS. Same direct-table lever as the decode side. LEVER: encode_dbcs2 now takes a direct `code point -> packed` table (`[u32; 0x10000]`, storing `packed + 1` so 0 = unrepresentable — packed 0 is a valid single output byte and cannot be the sentinel), one O(1) index instead of the binary search. Each codec builds its table lazily once (OnceLock) via build_enc_direct from the same sorted array (byte-for-byte identical); astral code points are not in any 2-byte DBCS so they stay Unrepresentable, matching the binary search missing them. A small dbcs_encoder! macro generates the 5 codec wrappers (GBK / EUC-KR / CP949 / GB2312 / Johab); Shift-JIS / Big5 keep theirs. Before -> after (release, UTF-8 -> DBCS, vs glibc): Big5: 5.70 -> 3.30 ns/byte (1.73x faster; 3.69x -> 2.87x vs glibc) Shift-JIS: 5.52 -> 4.65 ns/byte (1.19x; smaller table = less binary-search cost) EUC-JP encode is already ~parity with glibc (its own encoder, untouched). Isomorphism: iconv_differential_fuzz (covers Shift-JIS/Big5 vs live glibc) + conformance_diff_iconv + iconv_abi_test (49) all green. The direct table yields the same packed value as the binary search for every code point. Residual (the ~2.9-3x still vs glibc): the scalar UTF-8 source decode + per-char encode call — a SIMD-decode-feeds-DBCS-encode lever is the next step. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 229157c commit 6750bb6

1 file changed

Lines changed: 49 additions & 12 deletions

File tree

  • crates/frankenlibc-core/src/iconv

crates/frankenlibc-core/src/iconv/mod.rs

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8518,11 +8518,28 @@ fn decode_dbcs2(
85188518
/// `enc` table holds glibc's canonical encoding for every representable BMP code
85198519
/// point: a packed value `< 0x100` is a single output byte, a larger value is
85208520
/// `(b0<<8)|b1`. An absent code point is `Unrepresentable` (EILSEQ).
8521-
fn encode_dbcs2(ch: char, out: &mut [u8], enc: &[(u32, u32)]) -> Result<usize, EncodeError> {
8521+
/// Build a direct `code point -> packed encoding` table (`[u32; 0x10000]`) from a
8522+
/// sorted `(cp, packed)` DBCS encode table, storing `packed + 1` so 0 means
8523+
/// "unrepresentable" (a `packed` of 0 is a valid single output byte, so it cannot
8524+
/// itself be the sentinel). Replaces a per-char binary search (~13-14 cache-missing
8525+
/// probes over thousands of entries) with one O(1) index. Astral code points
8526+
/// (>= 0x10000) are not in any 2-byte DBCS, so they are not tabled and stay
8527+
/// `Unrepresentable` — identical to the binary search missing them.
8528+
fn build_enc_direct(enc: &[(u32, u32)]) -> Vec<u32> {
8529+
let mut t = vec![0u32; 0x10000];
8530+
for &(cp, packed) in enc {
8531+
if cp < 0x10000 {
8532+
t[cp as usize] = packed + 1;
8533+
}
8534+
}
8535+
t
8536+
}
8537+
8538+
fn encode_dbcs2(ch: char, out: &mut [u8], enc_direct: &[u32]) -> Result<usize, EncodeError> {
85228539
let cp = ch as u32;
8523-
match enc.binary_search_by_key(&cp, |&(c, _)| c) {
8524-
Ok(i) => {
8525-
let packed = enc[i].1;
8540+
if cp < 0x10000 && enc_direct[cp as usize] != 0 {
8541+
let packed = enc_direct[cp as usize] - 1;
8542+
{
85268543
if packed < 0x100 {
85278544
if out.is_empty() {
85288545
return Err(EncodeError::NoSpace);
@@ -8538,7 +8555,8 @@ fn encode_dbcs2(ch: char, out: &mut [u8], enc: &[(u32, u32)]) -> Result<usize, E
85388555
Ok(2)
85398556
}
85408557
}
8541-
Err(_) => Err(EncodeError::Unrepresentable),
8558+
} else {
8559+
Err(EncodeError::Unrepresentable)
85428560
}
85438561
}
85448562

@@ -8554,7 +8572,9 @@ fn decode_shiftjis(input: &[u8]) -> Result<(char, usize), DecodeError> {
85548572
}
85558573

85568574
fn encode_shiftjis(ch: char, out: &mut [u8]) -> Result<usize, EncodeError> {
8557-
encode_dbcs2(ch, out, &cjk_tables::SHIFT_JIS_ENC)
8575+
static DIRECT: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
8576+
let direct = DIRECT.get_or_init(|| build_enc_direct(&cjk_tables::SHIFT_JIS_ENC));
8577+
encode_dbcs2(ch, out, direct)
85588578
}
85598579

85608580
fn decode_big5(input: &[u8]) -> Result<(char, usize), DecodeError> {
@@ -8569,7 +8589,9 @@ fn decode_big5(input: &[u8]) -> Result<(char, usize), DecodeError> {
85698589
}
85708590

85718591
fn encode_big5(ch: char, out: &mut [u8]) -> Result<usize, EncodeError> {
8572-
encode_dbcs2(ch, out, &cjk_tables::BIG5_ENC)
8592+
static DIRECT: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
8593+
let direct = DIRECT.get_or_init(|| build_enc_direct(&cjk_tables::BIG5_ENC));
8594+
encode_dbcs2(ch, out, direct)
85738595
}
85748596

85758597
fn decode_gbk(input: &[u8]) -> Result<(char, usize), DecodeError> {
@@ -9317,15 +9339,30 @@ fn encode_char(enc: Encoding, ch: char, out: &mut [u8]) -> Result<usize, EncodeE
93179339
Encoding::EucJp => encode_eucjp(ch, out),
93189340
Encoding::ShiftJis => encode_shiftjis(ch, out),
93199341
Encoding::Big5 => encode_big5(ch, out),
9320-
Encoding::Gbk => encode_dbcs2(ch, out, &cjk_tables::GBK_ENC),
9321-
Encoding::EucKr => encode_dbcs2(ch, out, &cjk_tables::EUC_KR_ENC),
9322-
Encoding::Cp949 => encode_dbcs2(ch, out, &cjk_tables::CP949_ENC),
9323-
Encoding::Gb2312 => encode_dbcs2(ch, out, &cjk_tables::GB2312_ENC),
9342+
Encoding::Gbk => encode_gbk(ch, out),
9343+
Encoding::EucKr => encode_euckr(ch, out),
9344+
Encoding::Cp949 => encode_cp949(ch, out),
9345+
Encoding::Gb2312 => encode_gb2312(ch, out),
93249346
Encoding::Gb18030 => encode_gb18030(ch, out),
9325-
Encoding::Johab => encode_dbcs2(ch, out, &cjk_tables::JOHAB_ENC),
9347+
Encoding::Johab => encode_johab(ch, out),
93269348
}
93279349
}
93289350

9351+
macro_rules! dbcs_encoder {
9352+
($name:ident, $table:ident) => {
9353+
fn $name(ch: char, out: &mut [u8]) -> Result<usize, EncodeError> {
9354+
static DIRECT: std::sync::OnceLock<Vec<u32>> = std::sync::OnceLock::new();
9355+
let direct = DIRECT.get_or_init(|| build_enc_direct(&cjk_tables::$table));
9356+
encode_dbcs2(ch, out, direct)
9357+
}
9358+
};
9359+
}
9360+
dbcs_encoder!(encode_gbk, GBK_ENC);
9361+
dbcs_encoder!(encode_euckr, EUC_KR_ENC);
9362+
dbcs_encoder!(encode_cp949, CP949_ENC);
9363+
dbcs_encoder!(encode_gb2312, GB2312_ENC);
9364+
dbcs_encoder!(encode_johab, JOHAB_ENC);
9365+
93299366
/// Opens a character set conversion descriptor with deterministic dispatch metadata.
93309367
///
93319368
/// Equivalent to C `iconv_open`. Converts from `fromcode` encoding to `tocode` encoding.

0 commit comments

Comments
 (0)