Skip to content

Commit f044460

Browse files
committed
feat(gen): replace function name pointer array with blob + offset table
Replace kFnNames[] (const char * const pointer array) with a single contiguous kFnNameData[] string blob and a parallel kFnNameOffsets[] offset table (uint16_t when total blob size fits, uint32_t otherwise). On 64-bit PIC builds (the Linux default), each pointer array entry cost 8 bytes (pointer) + 24 bytes (R_X86_64_RELATIVE relocation) = 32 bytes per command. The offset table costs 2 bytes per command with zero relocations. Object file: 67,664 → 43,272 bytes (-36%) Binary: 76,176 → 51,528 bytes (-32%) Runtime cost is unchanged — one pointer addition (base + offset) replaces one pointer load from the array. Signed-off-by: Steven Noonan <steven@uplinklabs.net>
1 parent 32ea94e commit f044460

2 files changed

Lines changed: 66 additions & 20 deletions

File tree

src/generator/c/mod.rs

Lines changed: 38 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,35 @@ pub fn generate(
2929
let env = build_env()?;
3030
let preamble = preamble::build_preamble(fs, command_line);
3131

32+
// Compute function name string blob layout: each command name is stored
33+
// as a NUL-terminated string in a single contiguous char array, with a
34+
// parallel offset table for O(1) indexing. This avoids one pointer +
35+
// relocation per command (saves ~30 bytes/command on PIC builds).
36+
let fn_name_offsets: Vec<u32> = {
37+
let mut offsets = Vec::with_capacity(fs.commands.len());
38+
let mut pos = 0u32;
39+
for cmd in &fs.commands {
40+
offsets.push(pos);
41+
pos += cmd.name.len() as u32 + 1; // +1 for NUL
42+
}
43+
offsets
44+
};
45+
let fn_name_blob_size: u32 = fn_name_offsets
46+
.last()
47+
.map(|&last_off| {
48+
last_off
49+
+ fs.commands
50+
.last()
51+
.map(|c| c.name.len() as u32 + 1)
52+
.unwrap_or(0)
53+
})
54+
.unwrap_or(0);
55+
let fn_name_offset_type = if fn_name_blob_size <= u16::MAX as u32 {
56+
"uint16_t"
57+
} else {
58+
"uint32_t"
59+
};
60+
3261
// Output tree:
3362
// {out}/include/gloam/{stem}.h
3463
// {out}/include/KHR/khrplatform.h (and other aux headers)
@@ -40,12 +69,15 @@ pub fn generate(
4069
std::fs::create_dir_all(&src_dir)?;
4170

4271
let ctx = context! {
43-
fs => fs,
44-
stem => &stem,
45-
guard => format!("{}_H", stem.to_uppercase()),
46-
alias => args.alias,
47-
loader => args.loader,
48-
preamble => &preamble,
72+
fs => fs,
73+
stem => &stem,
74+
guard => format!("{}_H", stem.to_uppercase()),
75+
alias => args.alias,
76+
loader => args.loader,
77+
preamble => &preamble,
78+
fn_name_offsets => &fn_name_offsets,
79+
fn_name_blob_size => fn_name_blob_size,
80+
fn_name_offset_type => fn_name_offset_type,
4981
};
5082

5183
std::fs::write(

src/generator/c/templates/source.c.j2

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -47,17 +47,27 @@
4747
#endif
4848

4949
/* ---- Function name table -------------------------------------------------
50-
Indexed by command.index. Used for bootstrapping and by the PFN-range
51-
loader to call getProcAddr(name) for each slot. */
52-
static const char * const kFnNames_{{ fs.spec_name | spec_display }}[] = {
50+
Command names stored as a single NUL-terminated string blob with a parallel
51+
offset table for O(1) indexing. This avoids one pointer (8 bytes on 64-bit)
52+
plus one relocation entry (~24 bytes in PIC builds) per command compared to
53+
the traditional const char * const [] approach. */
54+
#define kFnCount_{{ fs.spec_name | spec_display }} {{ fs.commands | length }}
55+
56+
static const char kFnNameData_{{ fs.spec_name | spec_display }}[] =
57+
{%- for cmd in fs.commands %}
58+
/* {{ fn_name_offsets[cmd.index] | rjust(5) }} */ "{{ cmd.name }}\0"
59+
{%- endfor %}
60+
;
61+
62+
static const {{ fn_name_offset_type }} kFnNameOffsets_{{ fs.spec_name | spec_display }}[] = {
5363
{%- for cmd in fs.commands %}
54-
/* {{ cmd.index | rjust(4) }} */ "{{ cmd.name }}"{% if not loop.last %},{% endif %}
64+
/* {{ cmd.index | rjust(4) }} */ {{ fn_name_offsets[cmd.index] | rjust(5) }}{% if not loop.last %},{% endif %} /* {{ cmd.name }} */
5565
{%- endfor %}
5666
};
5767

5868
{% if fs.is_vulkan -%}
5969
/* ---- Command scope table -------------------------------------------------
60-
Indexed in lockstep with kFnNames_{{ fs.spec_name | spec_display }}[]. Each entry is the GloamCommandScope
70+
Indexed in lockstep with kFnNameOffsets_{{ fs.spec_name | spec_display }}[]. Each entry is the GloamCommandScope
6171
value for that slot — stored as uint8_t so the whole table is one byte per
6272
command (compares well against the alternatives: a parallel pointer array
6373
or a switch inside the loop). */
@@ -95,8 +105,10 @@ static const GloamPfnRange_t kFeatPfnRanges_{{ fs.spec_name | spec_display }}[]
95105
static void gloam_load_pfn_range_{{ fs.spec_name }}({{ u.ctx_arg(', ') }}GloamLoadFunc getProcAddr,
96106
uint16_t start, uint16_t count) {
97107
uint16_t i;
98-
for (i = start; i < (uint16_t)(start + count); ++i)
99-
context->pfnArray[i] = (void *)getProcAddr(kFnNames_{{ fs.spec_name | spec_display }}[i]);
108+
for (i = start; i < (uint16_t)(start + count); ++i) {
109+
const char *pfnName = &kFnNameData_{{ fs.spec_name | spec_display }}[kFnNameOffsets_{{ fs.spec_name | spec_display }}[i]];
110+
context->pfnArray[i] = (void *)getProcAddr(pfnName);
111+
}
100112
}
101113
{% else -%}
102114
/* ---- Vulkan scope-aware PFN range helper ---------------------------------
@@ -110,9 +122,11 @@ static void gloam_load_pfn_range_{{ fs.spec_name }}({{ u.ctx_arg(', ') }}GloamLo
110122
static void gloam_load_pfn_range_{{ fs.spec_name }}({{ u.ctx_arg(', ') }}GloamVkUserptrLoadFunc load,
111123
void *userptr, uint16_t start, uint16_t count) {
112124
uint16_t i;
113-
for (i = start; i < (uint16_t)(start + count); ++i)
114-
context->pfnArray[i] = (void *)load(userptr, kFnNames_{{ fs.spec_name | spec_display }}[i],
115-
(GloamCommandScope)kCommandScopes_{{ fs.spec_name | spec_display }}[i]);
125+
for (i = start; i < (uint16_t)(start + count); ++i) {
126+
const char *pfnName = &kFnNameData_{{ fs.spec_name | spec_display }}[kFnNameOffsets_{{ fs.spec_name | spec_display }}[i]];
127+
const GloamCommandScope cmdScope = (GloamCommandScope)kCommandScopes_{{ fs.spec_name | spec_display }}[i];
128+
context->pfnArray[i] = (void *)load(userptr, pfnName, cmdScope);
129+
}
116130
}
117131
{% endif %}
118132
{% if alias and fs.alias_pairs | length > 0 -%}
@@ -719,8 +733,8 @@ int gloamLoad{{ api | api_display }}Context({{ u.ctx_arg(', ') }}Display *displa
719733
if (!version) return 0;
720734

721735
/* Load all PFNs upfront. */
722-
for (i = 0; i < GLOAM_ARRAYSIZE(kFnNames_{{ fs.spec_name | spec_display }}); ++i)
723-
context->pfnArray[i] = (void *)getProcAddr(kFnNames_{{ fs.spec_name | spec_display }}[i]);
736+
for (i = 0; i < kFnCount_{{ fs.spec_name | spec_display }}; ++i)
737+
context->pfnArray[i] = (void *)getProcAddr((kFnNameData_{{ fs.spec_name | spec_display }} + kFnNameOffsets_{{ fs.spec_name | spec_display }}[i]));
724738

725739
/* Mark features based on PFN availability. */
726740
for (i = 0; i < GLOAM_ARRAYSIZE(kFeatPfnRanges_{{ fs.spec_name | spec_display }}); ++i) {
@@ -765,8 +779,8 @@ int gloamLoad{{ api | api_display }}Context({{ u.ctx_arg(', ') }}HDC hdc, GloamL
765779
if (!version) return 0;
766780

767781
/* Load all PFNs upfront. */
768-
for (i = 0; i < GLOAM_ARRAYSIZE(kFnNames_{{ fs.spec_name | spec_display }}); ++i)
769-
context->pfnArray[i] = (void *)getProcAddr(kFnNames_{{ fs.spec_name | spec_display }}[i]);
782+
for (i = 0; i < kFnCount_{{ fs.spec_name | spec_display }}; ++i)
783+
context->pfnArray[i] = (void *)getProcAddr((kFnNameData_{{ fs.spec_name | spec_display }} + kFnNameOffsets_{{ fs.spec_name | spec_display }}[i]));
770784

771785
/* Mark features based on PFN availability. */
772786
for (i = 0; i < GLOAM_ARRAYSIZE(kFeatPfnRanges_{{ fs.spec_name | spec_display }}); ++i) {

0 commit comments

Comments
 (0)