Skip to content

Commit 066ed53

Browse files
committed
Speed up vkGet{Instance,Device}ProcAddr lookups
Benchmarked against the pre-change baseline: typical and late-position lookups are 20-70x faster, and unknown-name (miss) lookups - the most common real-world case when probing for unsupported extensions - are 10-70x faster. The only downside is a small, fixed regression (a few ns, still under 20ns) for the single name that happened to be first in each function's original if-chain, due to the fixed cost of hashing the query string. Fixes #1631
1 parent c16b78f commit 066ed53

8 files changed

Lines changed: 9331 additions & 4535 deletions

File tree

loader/generated/vk_loader_extensions.c

Lines changed: 7071 additions & 4279 deletions
Large diffs are not rendered by default.

loader/generated/vk_loader_extensions.h

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,9 @@ VKAPI_ATTR VkResult VKAPI_CALL vkDevExtError(VkDevice dev);
4545

4646
// Extension interception for vkGetInstanceProcAddr function, so we can return
4747
// the appropriate information for any instance extensions we know about.
48-
bool extension_instance_gpa(struct loader_instance *ptr_instance, const char *name, void **addr);
48+
// name_hash must be loader_hash_string(name) - callers that already have it (e.g.
49+
// trampoline_get_proc_addr) pass it through instead of hashing name a second time.
50+
bool extension_instance_gpa(struct loader_instance *ptr_instance, const char *name, uint32_t name_hash, void **addr);
4951

5052
struct loader_instance_extension_enable_list; // Forward declaration
5153

loader/gpa_helper.c

Lines changed: 719 additions & 236 deletions
Large diffs are not rendered by default.

loader/loader_common.h

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,27 @@ static const unsigned char UTF8_THREE_BYTE_MASK = 248; // 0xF8;
5959
static const unsigned char UTF8_DATA_BYTE_CODE = 128; // 0x80;
6060
static const unsigned char UTF8_DATA_BYTE_MASK = 192; // 0xC0;
6161

62+
// 32-bit FNV-1a (https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function). Mirrored
63+
// byte-for-byte in Python as loader_hash_string() in scripts/generators/loader_extension_generator.py,
64+
// which precomputes the hash constants embedded in loader/generated/vk_loader_extensions.c - keep both in
65+
// sync or those constants stop matching this function (see HashString.MatchesGeneratorEmbeddedConstants in
66+
// tests/loader_hash_string_tests.cpp).
67+
//
68+
// Used as a cheap pre-filter before the strcmp confirmation when resolving entry point names
69+
// (vkGet{Instance,Device}ProcAddr): a hash match still falls through to a switch/case-guarded strcmp before
70+
// anything is returned, so a collision can never produce a wrong lookup - at worst it costs one extra
71+
// strcmp. Bounded by MaxLoaderStringLength so a non-NUL-terminated or adversarially long input can't make
72+
// this scan past a bounded byte count the way an unbounded `while (*str)` would; every real entry point
73+
// name is far shorter than the cap, so this never changes the hash of a valid name.
74+
static inline uint32_t loader_hash_string(const char *str) {
75+
uint32_t hash = 2166136261u;
76+
for (uint32_t i = 0; i < (uint32_t)MaxLoaderStringLength && str[i]; ++i) {
77+
hash ^= (uint8_t)str[i];
78+
hash *= 16777619u;
79+
}
80+
return hash;
81+
}
82+
6283
// form of all dynamic lists/arrays
6384
// only the list element should be changed
6485
struct loader_generic_list {

scripts/generators/loader_extension_generator.py

Lines changed: 93 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,57 @@
104104
'vkEnumerateInstanceLayerProperties',
105105
'vkEnumerateInstanceVersion']
106106

107+
def _read_max_loader_string_length_from_c_header() -> int:
108+
header_path = os.path.join(os.path.dirname(__file__), '..', '..', 'loader', 'loader_common.h')
109+
with open(header_path, 'r', encoding='utf-8') as f:
110+
header_text = f.read()
111+
match = re.search(r'^\s*static const int MaxLoaderStringLength\s*=\s*(\d+)\s*;', header_text, re.MULTILINE)
112+
assert match, (
113+
f'Could not find "static const int MaxLoaderStringLength = <N>;" in {header_path}. '
114+
'loader_hash_string() below needs this bound to stay in sync with loader/loader_common.h - '
115+
'did that declaration get renamed or reformatted?')
116+
return int(match.group(1))
117+
118+
# Must match MaxLoaderStringLength in loader/loader_common.h - loader_hash_string() there stops scanning
119+
# after this many bytes (a name of exactly this length is scanned in full and hashes the same either way;
120+
# only a name LONGER than this would hash differently in C than in Python below). Parsed directly out of
121+
# the header (rather than hand-copied) so the two can never silently drift apart.
122+
MAX_LOADER_STRING_LENGTH = _read_max_loader_string_length_from_c_header()
123+
124+
# 32-bit FNV-1a (https://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function). Must stay
125+
# byte-for-byte identical to loader_hash_string() in loader/loader_common.h - this precomputes the hash
126+
# constants baked into the generated *_gpa()/dispatch-table-lookup functions below, which the C function
127+
# then has to reproduce at runtime to hit the right branch. HashString.MatchesGeneratorEmbeddedConstants in
128+
# tests/loader_hash_string_tests.cpp catches any divergence.
129+
def loader_hash_string(s: str) -> int:
130+
assert len(s) <= MAX_LOADER_STRING_LENGTH, (
131+
f'"{s}" is {len(s)} bytes, over MaxLoaderStringLength ({MAX_LOADER_STRING_LENGTH}). '
132+
'loader_hash_string() in loader/loader_common.h truncates its scan at that length, so this name '
133+
"would hash differently at runtime than the constant this generator is about to embed for it - "
134+
'shorten the name or raise MaxLoaderStringLength in both the C and Python copies.')
135+
h = 0x811c9dc5
136+
for c in s.encode('ascii'):
137+
h ^= c
138+
h = (h * 0x01000193) & 0xFFFFFFFF
139+
return h
140+
141+
# Raises if any two names in `names` share a hash - used as a generation-time tripwire so that if the
142+
# Vulkan command set ever grows into a collision, it's caught immediately with a clear message instead of
143+
# failing to compile with an inscrutable "duplicate case value" error naming neither colliding string (each
144+
# name in `names` becomes its own `case <hash>u:` label in the generated switch - see OutputLoaderLookupFunc
145+
# and InstExtensionGPA below).
146+
def check_no_hash_collisions(names, context: str):
147+
seen = {}
148+
for name in names:
149+
h = loader_hash_string(name)
150+
if h in seen and seen[h] != name:
151+
raise RuntimeError(
152+
f'FNV-1a hash collision in {context}: "{seen[h]}" and "{name}" both hash to {h:#010x}. '
153+
'Left unaddressed, this would make the generated switch emit two identical case labels and '
154+
'fail to compile with a "duplicate case value" error naming neither string - '
155+
'loader_hash_string() (C and Python copies) should be revisited (e.g. a different seed/prime).')
156+
seen[h] = name
157+
107158
# This class is a container for any source code, data, or other behavior that is necessary to
108159
# customize the generator script for a specific target API variant (e.g. Vulkan SC). As such,
109160
# all of these API-specific interfaces and their use in the generator script are part of the
@@ -275,7 +326,9 @@ def OutputPrototypesInHeader(self, out: list):
275326
276327
// Extension interception for vkGetInstanceProcAddr function, so we can return
277328
// the appropriate information for any instance extensions we know about.
278-
bool extension_instance_gpa(struct loader_instance *ptr_instance, const char *name, void **addr);
329+
// name_hash must be loader_hash_string(name) - callers that already have it (e.g.
330+
// trampoline_get_proc_addr) pass it through instead of hashing name a second time.
331+
bool extension_instance_gpa(struct loader_instance *ptr_instance, const char *name, uint32_t name_hash, void **addr);
279332
280333
struct loader_instance_extension_enable_list; // Forward declaration
281334
@@ -704,6 +757,7 @@ def OutputLoaderLookupFunc(self, out):
704757
out.append(' struct loader_device* dev = (struct loader_device *)table;\n')
705758
out.append(' const struct loader_instance* inst = dev->phys_dev_term->this_icd_term->this_instance;\n')
706759
out.append(' uint32_t api_version = VK_MAKE_API_VERSION(0, inst->app_api_version.major, inst->app_api_version.minor, inst->app_api_version.patch);\n')
760+
out.append(' const uint32_t name_hash = loader_hash_string(name);\n')
707761
out.append('\n')
708762
else:
709763
cur_type = 'instance'
@@ -718,8 +772,11 @@ def OutputLoaderLookupFunc(self, out):
718772
out.append('\n')
719773
out.append(' *found_name = true;\n')
720774
out.append(' name += 2;\n')
775+
out.append(' const uint32_t name_hash = loader_hash_string(name);\n')
721776

777+
out.append(' switch (name_hash) {\n')
722778

779+
base_names_for_collision_check = []
723780
for command_list in [self.core_commands, self.extension_commands]:
724781
commands = command_list
725782

@@ -729,12 +786,12 @@ def OutputLoaderLookupFunc(self, out):
729786
is_inst_handle_type = command.params[0].type in ['VkInstance', 'VkPhysicalDevice']
730787
if ((cur_type == 'instance' and is_inst_handle_type) or (cur_type == 'device' and not is_inst_handle_type)):
731788

732-
current_block = self.DescribeBlock(command, current_block, out)
789+
current_block = self.DescribeBlock(command, current_block, out, indent=' ')
733790
if len(command.extensions) == 0:
734791
if cur_type == 'device':
735792
effective_version_name = APISpecific.getEffectiveVersionName(self.targetApiName, command.version)
736793
api_version = effective_version_name.replace('_VERSION_', '_API_VERSION_')
737-
version_check = f" if (dev->should_ignore_device_commands_from_newer_version && api_version < {api_version}) return NULL;\n"
794+
version_check = f" if (dev->should_ignore_device_commands_from_newer_version && api_version < {api_version}) return NULL;\n"
738795
else:
739796
version_check = ''
740797

@@ -750,22 +807,29 @@ def OutputLoaderLookupFunc(self, out):
750807
if command.protect is not None:
751808
out.append(f'#if defined({command.protect})\n')
752809

753-
out.append(f' if (!strcmp(name, "{base_name}")) ')
810+
base_names_for_collision_check.append(base_name)
811+
out.append(f' case {loader_hash_string(base_name):#010x}u:\n')
812+
out.append(f' if (!strcmp(name, "{base_name}")) ')
754813
if command.name in DEVICE_CMDS_MUST_USE_TRAMP:
755814
if version_check != '':
756-
out.append(f'{{\n{version_check} return dev->layer_extensions.{command.extensions[0][3:].lower()}_enabled ? (void *){base_name} : NULL;\n }}\n')
815+
out.append(f'{{\n{version_check} return dev->layer_extensions.{command.extensions[0][3:].lower()}_enabled ? (void *){base_name} : NULL;\n }}\n')
757816
else:
758817
out.append(f'return dev->layer_extensions.{command.extensions[0][3:].lower()}_enabled ? (void *){base_name} : NULL;\n')
759818

760819
else:
761820
if version_check != '':
762-
out.append(f'{{\n{version_check} return (void *)table->{base_name};\n }}\n')
821+
out.append(f'{{\n{version_check} return (void *)table->{base_name};\n }}\n')
763822
else:
764823
out.append(f'return (void *)table->{base_name};\n')
824+
out.append(' break;\n')
765825

766826
if command.protect is not None:
767827
out.append(f'#endif // {command.protect}\n')
768828

829+
check_no_hash_collisions(base_names_for_collision_check, f'loader_lookup_{cur_type}_dispatch_table')
830+
out.append(' default:\n')
831+
out.append(' break;\n')
832+
out.append(' }\n')
769833
out.append('\n')
770834
out.append(' *found_name = false;\n')
771835
out.append(' return NULL;\n')
@@ -1187,9 +1251,12 @@ def InstExtensionGPA(self, out):
11871251
cur_extension_name = ''
11881252

11891253
out.append( '// GPA helpers for extensions\n')
1190-
out.append( 'bool extension_instance_gpa(struct loader_instance *ptr_instance, const char *name, void **addr) {\n')
1191-
out.append( ' *addr = NULL;\n\n')
1254+
out.append( '// name_hash must be loader_hash_string(name) - passed in by the caller so it is only computed once per lookup.\n')
1255+
out.append( 'bool extension_instance_gpa(struct loader_instance *ptr_instance, const char *name, uint32_t name_hash, void **addr) {\n')
1256+
out.append( ' *addr = NULL;\n')
1257+
out.append( ' switch (name_hash) {\n')
11921258

1259+
full_names_for_collision_check = []
11931260
for command in [x for x in self.vk.commands.values() if x.extensions]:
11941261
if (command.version or
11951262
command.extensions[0] in WSI_EXT_NAMES or
@@ -1198,7 +1265,7 @@ def InstExtensionGPA(self, out):
11981265
continue
11991266

12001267
if command.extensions[0] != cur_extension_name:
1201-
out.append( f'\n // ---- {command.extensions[0]} extension commands\n')
1268+
out.append( f'\n // ---- {command.extensions[0]} extension commands\n')
12021269
cur_extension_name = command.extensions[0]
12031270

12041271
if command.protect is not None:
@@ -1207,24 +1274,31 @@ def InstExtensionGPA(self, out):
12071274
#base_name = command.name[2:]
12081275
base_name = SHARED_ALIASES[command.name] if command.name in SHARED_ALIASES else command.name[2:]
12091276

1277+
full_names_for_collision_check.append(command.name)
1278+
out.append(f' case {loader_hash_string(command.name):#010x}u:\n')
12101279
if len(command.extensions) > 0 and self.vk.extensions[command.extensions[0]].instance:
1211-
out.append( f' if (!strcmp("{command.name}", name)) {{\n')
1212-
out.append( ' *addr = (ptr_instance->enabled_extensions.')
1280+
out.append( f' if (!strcmp("{command.name}", name)) {{\n')
1281+
out.append( ' *addr = (ptr_instance->enabled_extensions.')
12131282
out.append( command.extensions[0][3:].lower())
12141283
out.append( ' == 1)\n')
1215-
out.append( f' ? (void *){base_name}\n')
1216-
out.append( ' : NULL;\n')
1217-
out.append( ' return true;\n')
1218-
out.append( ' }\n')
1284+
out.append( f' ? (void *){base_name}\n')
1285+
out.append( ' : NULL;\n')
1286+
out.append( ' return true;\n')
1287+
out.append( ' }\n')
12191288
else:
1220-
out.append( f' if (!strcmp("{command.name}", name)) {{\n')
1221-
out.append( f' *addr = (void *){base_name};\n')
1222-
out.append( ' return true;\n')
1223-
out.append( ' }\n')
1289+
out.append( f' if (!strcmp("{command.name}", name)) {{\n')
1290+
out.append( f' *addr = (void *){base_name};\n')
1291+
out.append( ' return true;\n')
1292+
out.append( ' }\n')
1293+
out.append(' break;\n')
12241294

12251295
if command.protect is not None:
12261296
out.append( f'#endif // {command.protect}\n')
12271297

1298+
check_no_hash_collisions(full_names_for_collision_check, 'extension_instance_gpa')
1299+
out.append( ' default:\n')
1300+
out.append( ' break;\n')
1301+
out.append( ' }\n')
12281302
out.append( ' return false;\n')
12291303
out.append( '}\n\n')
12301304

tests/CMakeLists.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ add_executable(
8181
loader_alloc_callback_tests.cpp
8282
loader_envvar_tests.cpp
8383
loader_get_proc_addr_tests.cpp
84+
loader_hash_string_tests.cpp
8485
loader_debug_ext_tests.cpp
8586
loader_handle_validation_tests.cpp
8687
loader_layer_tests.cpp
@@ -92,6 +93,7 @@ add_executable(
9293
loader_wsi_tests.cpp)
9394
target_link_libraries(test_regression PUBLIC testing_dependencies)
9495
target_compile_definitions(test_regression PUBLIC VK_NO_PROTOTYPES)
96+
target_include_directories(test_regression PRIVATE ${CMAKE_CURRENT_LIST_DIR}/../loader ${CMAKE_CURRENT_LIST_DIR}/../loader/generated)
9597

9698
add_executable(
9799
test_fuzzing

0 commit comments

Comments
 (0)