Skip to content

Optimize StaticStringMap Implementation #21498

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

RetroDev256
Copy link
Contributor

@RetroDev256 RetroDev256 commented Sep 23, 2024

Optimized StaticStringMap

StaticStringMap is an optimized mapping between some key/value pairs. The keys are always strings, and the values can be any type. The structure is initialized with the key/value pairs, and there is no way to change key/value pairs through the lifetime of the type.

This PR replaces the implementation of StaticStringMap.

Runtime init() has been removed. Use initComptime() instead.
Functions getLongestPrefix() and getLongestPrefixIndex() have been removed.
Type StaticStringMapWithEql is no longer pub.
Type StaticStringMapIgnoreCaseAscii has been added. It is a case-insensitive StaticStringMap.
StaticStringMap now checks for redundant keys.
Test cases were removed if they referenced removed features.

The motivation behind this PR is a simplified implementation and improved code generation.
Here is the code generation: https://godbolt.org/z/ojW841qzh
The performance of StaticStringMap has more than doubled for the LLVM backend:
llvm_bench
bench.zip

x86_64 backend benchmark for `get` and `has`

x86_bench

Zig built with LLVM running `zig fmt CodeGen.zig`

llvm_backend_fmt

Zig built with x86_64 backend running `zig fmt CodeGen.zig`

x86_backend_fmt

Zig built with LLVM compiling "hello world"

llvm_backend_hello

Zig built with x86_64 backend compiling "hello world"

x86_backend_hello

Zig built with LLVM running "zig ast-check CodeGen.zig"

llvm_backend_ast

Zig built with x86_64 backend running "zig ast-check CodeGen.zig"

x86_backend_ast

Size difference in Zig compiled with LLVM backend

llvm_backend_size

Size difference in Zig compiled with x86_64 backend

x86_backend_size

Building the benchmark of both implementations

building_impls

Closes #20055

@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch 2 times, most recently from be28052 to 4de75ff Compare September 23, 2024 20:10
@nektro
Copy link
Contributor

nektro commented Sep 23, 2024

impressive results! can you post the benchmark(s) source code and also expose a version that lets u8 be generic as well? eg uses []const K

@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch 2 times, most recently from 08a1339 to 6ce870d Compare September 23, 2024 22:00
@travisstaloch
Copy link
Contributor

travisstaloch commented Sep 23, 2024

Why remove the initComptime() and init() API? The idea behind #19719 was to move kvs_list from a type param to a init() param in order to avoid very long type names which result from them being a type param.

EDIT: I like the simplicity of this implementation and the results seem to speak for themselves 👍 . FWIW, I don't care about runtime init(). But I think keeping the StaticStringMap name and moving kvs_list to an initComptime() param might be preferrable.

@RetroDev256
Copy link
Contributor Author

RetroDev256 commented Sep 23, 2024

Why remove the initComptime() and init() API? The idea behind #19719 was to move kvs_list from a type param to a init() param in order to avoid very long type names which result from them being a type param.

EDIT: I like the simplicity of this implementation and the results seem to speak for themselves 👍 . FWIW, I don't care about runtime init(). But I think keeping the StaticStringMap name and moving kvs_list to an initComptime() param might be preferrable.

Thank you for explaining the original reasoning! It's well founded. - I'll work on that and see how soon I can get it working.

What are your thoughts on replacing initComptime() with init(), assuming we never use this anymore for runtime kvs?

@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch 3 times, most recently from 034277d to 56b7655 Compare September 24, 2024 00:37
@RetroDev256 RetroDev256 changed the title Yeet StaticStringMap, replace with ComptimeStringMap Replace StaticStringMap with far more optimized version Sep 24, 2024
Copy link
Member

@andrewrk andrewrk left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Impressive results.

Please document the breaking changes in the PR writeup (pretend you are writing release notes).

Please also explain why you deleted a lot of test coverage, otherwise it's difficult to be sure these changes don't break existing use cases.

@RetroDev256
Copy link
Contributor Author

Please document the breaking changes in the PR writeup (pretend you are writing release notes).

Please also explain why you deleted a lot of test coverage, otherwise it's difficult to be sure these changes don't break existing use cases.

Sounds good - let me know if you would like further changes.

@JonathanHallstrom
Copy link
Contributor

i started a review pointing out an issue in toLowerSimd but it doesnt seem to be working so I'll post it here:

    const true_vec: BVec = @splat(true);
    const is_upper: BVec = @select(bool, at_min, at_max, true_vec);

    const lowered: SVec = input + @as(SVec, @splat('a' - 'A'));

this has two issues: is_upper incorrectly identifying [0, 65) as being uppercase letters, and lowered potentially overflowing, which wouldn't be an error, so it should be +%. i propose you change it in the following way:

-    const true_vec: BVec = @splat(true);
-    const is_upper: BVec = @select(bool, at_min, at_max, true_vec);
+    const false_vec: BVec = @splat(false);
+    const is_upper: BVec = @select(bool, at_min, at_max, false_vec);

-    const lowered: SVec = input + @as(SVec, @splat('a' - 'A'));
+    const lowered: SVec = input +% @as(SVec, @splat('a' - 'A'));

comptime code to verify that the changed version works
if you change it to the old implementation of toLowerSimd you will see that it no longer compiles

@travisstaloch
Copy link
Contributor

That is correct. I don't think removing runtime init() is or the getLongestPrefix() functions is going to be a big deal. They were only added as extra, maybe nice to haves. And getLongestPrefix() was doing a linear scan so its probably better to remove it.

@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch 2 times, most recently from 3d6e6ef to 0143320 Compare December 25, 2024 16:39
@RetroDev256
Copy link
Contributor Author

I'm pretty sure the failures are unrelated, but I am curious about the aarch64-linux-release failure. Looks like it is incremental related. Is this test nondeterministic by any chance?

@mlugg
Copy link
Member

mlugg commented Dec 25, 2024

My working theory for our inconsistent CI failures is that some system condition when under load causes processes to frequently crash. That aligns with what we see here -- note that on the failed incremental test, we also get a BrokenPipe on another step, which likely indicates a crash.

It's possible there is an incremental test case which is sometimes crashing the compiler, but I'd have to see the crash on a *-debug CI run to know for sure.

I'll re-run the failed jobs when the in-progress one finishes.

EDIT: oh, that incremental test totally is actually flaky, my bad -- see this run. Will fix now!

@alexrp
Copy link
Member

alexrp commented Feb 7, 2025

test
+- test-modules
   +- test-behavior
      +- run test behavior-riscv64-linux.4.19...6.11.5-musl.2.28-baseline_rv64-Debug-selfhosted-no-lld
         +- zig test Debug riscv64-linux-musl 1 errors
/home/ci/actions-runner4/_work/zig/zig/lib/std/static_array_map.zig:27:5: error: TODO: airBitCast [3]u8 to u24
pub fn defaultEql(comptime expected: anytype, actual: anytype) bool {

@Rexicon226 👀

@RetroDev256
Copy link
Contributor Author

RetroDev256 commented Feb 7, 2025

There is a limitation to both the old approach, and the new approach. Custom equal functions cannot say that two different-length arrays/strings are equal, because in both implementations the length is compared before they even reach the equal function.

There are a few options we can take:

  1. Remove all "custom equal function" abstractions, and only provide "all bits match", or (for strings only), "case-insensitive string matching". With this option, we could also consider whether we even want StaticArrayMap, or if StaticStringMap is more than enough for the standard library. (StaticArrayMap is a YAGNI for most use cases).
  2. Modify the "WithEql" types to not pre-filter the input strings based on length. The upside is that we can now make custom equal functions that handle strings with different lengths. The downside is that comparing the strings takes longer, and the standard library is made more complicated.
  3. Narrow the definition of the custom eql function to say that it is meant to compare individual elements instead of the string as a whole.

The third option could be done in two ways: we could say "make a function that takes one comptime-known element and one runtime-known element, and it decides how to compare them". This has the distinct advantage that the interface is further simplified, but cripples the current advantage of the defaultEql function. The second way to accept the third option is to keep the current interface, changing nothing.

Personally I would prefer the first option, as I like simplicity. In my opinion, we don't need StaticArrayMap, but a comptime-optimized mapping between strings (normal or case-insensitive) is more than enough.

Thoughts?

I could make this into an issue if it's desired, but I figure it would be smoother to make such a change now rather than later.

@andrewrk
Copy link
Member

Personally I would prefer the first option, as I like simplicity. In my opinion, we don't need StaticArrayMap, but a comptime-optimized mapping between strings (normal or case-insensitive) is more than enough.

I agree.

@andrewrk
Copy link
Member

Please rebase against latest master branch and force push. When I do that with github ui I receive this:

Response.json: Body has already been consumed.

@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch 4 times, most recently from 4aefeb8 to 7f43808 Compare February 22, 2025 23:36
@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch 2 times, most recently from 2032978 to e709cd9 Compare February 26, 2025 01:43
@RetroDev256
Copy link
Contributor Author

I am interested in getting this landed for the 0.14.0 release, and would be happy to make any adjustments to facilitate this. Is there anything more that may be needed of me?

@andrewrk
Copy link
Member

it's a very messy writeup, comment history, commit history, which means it will require careful review, which means it will not have time to be merged before 0.14.0

@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch from e709cd9 to f643ea2 Compare June 10, 2025 06:12
@RetroDev256 RetroDev256 changed the title Replace StaticStringMap with far more optimized version Optimize StaticStringMap Implementation Jun 10, 2025
@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch from f643ea2 to 15d4be4 Compare June 10, 2025 17:56
No more runtime initialization, no more StaticStringMapWithEql.
Introduce StaticStringMapIgnoreCaseAscii.
Update references to StaticStringMapWithEql to use StaticStringMapIgnoreCaseAscii.
Update asn1 to conform to comptime-known requirements of StaticStringMap.get().
Skip testing std.meta.stringToEnum with the stage2_riscv64 backend.
@RetroDev256 RetroDev256 force-pushed the comptime-string-map branch from 15d4be4 to 4842774 Compare June 10, 2025 17:58
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

std.StaticStringMap.initComptime allows duplicate keys
10 participants