diff options
| author | Stefan Boberg <[email protected]> | 2026-04-16 14:22:09 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-16 14:22:09 +0200 |
| commit | db616a2ede14670423b7b2b28aa36c33cabb030e (patch) | |
| tree | ced9c440544f12961207a0465590b6df3d0609e9 | |
| parent | 5.8.5-pre1 (diff) | |
| download | archived-zen-db616a2ede14670423b7b2b28aa36c33cabb030e.tar.xz archived-zen-db616a2ede14670423b7b2b28aa36c33cabb030e.zip | |
Add reduce-allocs skill and string builder infrastructure (#937)
Adds infrastructure for reducing short-lived heap allocations, to be applied across the codebase in follow-up PRs.
- **`reduce-allocs` Claude Code skill** — reviews code for unnecessary heap allocations and suggests fixes using stack-friendly patterns (`ExtendableStringBuilder`, `eastl::fixed_vector`, `TRefCounted`, etc.)
- **`TransparentStringHash`** (`zencore/hashutils.h`) — enables `std::string_view` lookups on `std::string`-keyed `unordered_map` without allocating a temporary string (C++20 heterogeneous lookup via `is_transparent`)
- **`AppendPaddedInt()`** and **`AppendFill()`** on `StringBuilderBase` (`zencore/string.h`) — zero-padded integer formatting and repeated-character fills without going through `fmt::format`
- **`StringBuilderAppender`** output iterator adapter — allows `fmt::format_to` to write directly into a `StringBuilderBase`
| -rw-r--r-- | .claude/skills/reduce-allocs/SKILL.md | 35 | ||||
| -rw-r--r-- | src/zen/cmds/wipe_cmd.cpp | 1 | ||||
| -rw-r--r-- | src/zencore/include/zencore/fmtutils.h | 43 | ||||
| -rw-r--r-- | src/zencore/include/zencore/hashutils.h | 19 | ||||
| -rw-r--r-- | src/zencore/include/zencore/iohash.h | 1 | ||||
| -rw-r--r-- | src/zencore/include/zencore/string.h | 24 | ||||
| -rw-r--r-- | src/zencore/string.cpp | 34 |
7 files changed, 128 insertions, 29 deletions
diff --git a/.claude/skills/reduce-allocs/SKILL.md b/.claude/skills/reduce-allocs/SKILL.md new file mode 100644 index 000000000..ff8a37857 --- /dev/null +++ b/.claude/skills/reduce-allocs/SKILL.md @@ -0,0 +1,35 @@ +--- +name: reduce-allocs +description: Review code for unnecessary short-lived heap allocations and suggest fixes using stack-friendly patterns from the zen codebase (ExtendableStringBuilder, eastl::fixed_vector, TRefCounted, etc.) +allowed-tools: Read Grep Glob +user-invocable: true +argument-hint: [file-or-directory] +--- + +Review $ARGUMENTS for unnecessary short-lived memory allocations and suggest concrete improvements. + +## Patterns to look for + +- **`std::string` temporaries built via concatenation**: Replace with `ExtendableStringBuilder<N>` where N fits the common-case length, avoiding heap allocation entirely in the typical path +- **`fmt::format()` returning `std::string` for transient use**: Use `fmt::format_to()` with an `ExtendableStringBuilder` or stack buffer when the result is consumed immediately +- **`std::vector` for small or bounded collections**: Replace with `eastl::fixed_vector<T, N>` when the element count is typically small or has a known upper bound +- **`std::shared_ptr` where ownership policy is known at type-definition time**: Use `TRefCounted<T>` (or `RefCounted`) with `Ref<T>` to avoid the separate control-block allocation +- **Repeated small allocations inside tight loops**: Hoist buffers outside the loop, reuse via `.clear()`, or use stack-local storage +- **Temporary `std::string` passed only to `std::string_view` consumers**: Use `std::string_view` or a stack buffer directly +- **`std::string` in function arguments**: Prefer `std::string_view` for read-only parameters to avoid unnecessary copies + +## Guidelines + +- Only flag allocations that are genuinely short-lived or transient. Long-lived or shared strings that need heap storage are fine. +- Pick `N` for `ExtendableStringBuilder<N>` based on realistic expected sizes (e.g. 128 for typical paths, 256 for formatted messages, 64 for small labels). +- Do not change public API signatures unless the caller also benefits. +- Preserve correctness: ensure replacements handle edge cases like empty strings, null terminators, and overflow beyond N. + +## Output format + +For each finding: +1. File and line reference +2. Current code snippet +3. Why it allocates unnecessarily +4. Suggested replacement code +5. Any caveats or trade-offs diff --git a/src/zen/cmds/wipe_cmd.cpp b/src/zen/cmds/wipe_cmd.cpp index 10f5ad8e1..fe4e12906 100644 --- a/src/zen/cmds/wipe_cmd.cpp +++ b/src/zen/cmds/wipe_cmd.cpp @@ -4,6 +4,7 @@ #include <zencore/filesystem.h> #include <zencore/fmtutils.h> +#include <zencore/iohash.h> #include <zencore/logging.h> #include <zencore/parallelwork.h> #include <zencore/string.h> diff --git a/src/zencore/include/zencore/fmtutils.h b/src/zencore/include/zencore/fmtutils.h index 2a829d2d5..a263c6f04 100644 --- a/src/zencore/include/zencore/fmtutils.h +++ b/src/zencore/include/zencore/fmtutils.h @@ -3,10 +3,7 @@ #pragma once #include <zencore/filesystem.h> -#include <zencore/guid.h> -#include <zencore/iohash.h> #include <zencore/string.h> -#include <zencore/uid.h> ZEN_THIRD_PARTY_INCLUDES_START #include <fmt/format.h> @@ -60,39 +57,27 @@ struct fmt::formatter<T> : fmt::formatter<std::string_view> } }; -template<> -struct fmt::formatter<zen::IoHash> : formatter<string_view> -{ - template<typename FormatContext> - auto format(const zen::IoHash& Hash, FormatContext& ctx) const - { - zen::IoHash::String_t String; - Hash.ToHexString(String); - return fmt::formatter<string_view>::format({String, zen::IoHash::StringLength}, ctx); - } -}; +// Generic formatter for any type with a ToString(StringBuilderBase&) member function. +// This covers Guid, IoHash, Oid, and similar types without needing per-type +// fmt::formatter specializations. -template<> -struct fmt::formatter<zen::Oid> : formatter<string_view> +template<typename T> +concept HasMemberToStringBuilder = std::is_class_v<T> && requires(const T& v, zen::StringBuilderBase& sb) { - template<typename FormatContext> - auto format(const zen::Oid& Id, FormatContext& ctx) const { - zen::StringBuilder<32> String; - Id.ToString(String); - return fmt::formatter<string_view>::format({String.c_str(), zen::Oid::StringLength}, ctx); - } -}; + v.ToString(sb) + } -> std::same_as<zen::StringBuilderBase&>; +} && !HasFreeToString<T> && !HasStringViewConversion<T>; -template<> -struct fmt::formatter<zen::Guid> : formatter<string_view> +template<HasMemberToStringBuilder T> +struct fmt::formatter<T> : fmt::formatter<std::string_view> { template<typename FormatContext> - auto format(const zen::Guid& Id, FormatContext& ctx) const + auto format(const T& Value, FormatContext& ctx) const { - zen::StringBuilder<48> String; - Id.ToString(String); - return fmt::formatter<string_view>::format({String.c_str(), zen::Guid::StringLength}, ctx); + zen::ExtendableStringBuilder<64> String; + Value.ToString(String); + return fmt::formatter<std::string_view>::format(String.ToView(), ctx); } }; diff --git a/src/zencore/include/zencore/hashutils.h b/src/zencore/include/zencore/hashutils.h index 8abfd4b6e..e253d7015 100644 --- a/src/zencore/include/zencore/hashutils.h +++ b/src/zencore/include/zencore/hashutils.h @@ -4,6 +4,8 @@ #include <cstddef> #include <functional> +#include <string> +#include <string_view> #include <type_traits> namespace zen { @@ -35,4 +37,21 @@ CombineHashes(const Types&... Args) return Seed; } +/** Transparent string hash for use with std::unordered_map/set. + Enables heterogeneous lookup so that a std::string_view can be used to + probe a std::string-keyed container without allocating a temporary std::string. + + Usage: + std::unordered_map<std::string, V, TransparentStringHash, std::equal_to<>> Map; + Map.find(some_string_view); // no allocation + */ +struct TransparentStringHash +{ + using is_transparent = void; + + size_t operator()(std::string_view Sv) const noexcept { return std::hash<std::string_view>{}(Sv); } + size_t operator()(const std::string& S) const noexcept { return std::hash<std::string_view>{}(S); } + size_t operator()(const char* S) const noexcept { return std::hash<std::string_view>{}(S); } +}; + } // namespace zen diff --git a/src/zencore/include/zencore/iohash.h b/src/zencore/include/zencore/iohash.h index a619b0053..50c439b70 100644 --- a/src/zencore/include/zencore/iohash.h +++ b/src/zencore/include/zencore/iohash.h @@ -54,6 +54,7 @@ struct IoHash static bool TryParse(std::string_view Str, IoHash& Hash); const char* ToHexString(char* outString /* 40 characters + NUL terminator */) const; StringBuilderBase& ToHexString(StringBuilderBase& outBuilder) const; + StringBuilderBase& ToString(StringBuilderBase& outBuilder) const { return ToHexString(outBuilder); } std::string ToHexString() const; static constexpr int StringLength = 40; diff --git a/src/zencore/include/zencore/string.h b/src/zencore/include/zencore/string.h index 308a8a7d2..b4926070c 100644 --- a/src/zencore/include/zencore/string.h +++ b/src/zencore/include/zencore/string.h @@ -402,6 +402,12 @@ public: inline std::string_view ToView() const { return std::string_view(m_Base, m_CurPos - m_Base); } inline std::string ToString() const { return std::string{Data(), Size()}; } + /// Append a zero-padded decimal integer. MinWidth is the minimum number of digits (zero-padded on the left). + void AppendPaddedInt(int64_t Value, int MinWidth); + + /// Append a single character repeated Count times. + void AppendFill(char C, size_t Count); + inline void AppendCodepoint(uint32_t cp) { if (cp < 0x80) // one octet @@ -435,6 +441,24 @@ public: } }; +/// Output iterator adapter for StringBuilderBase, enabling direct use with fmt::format_to / fmt::format_to_n. +class StringBuilderAppender +{ + StringBuilderBase* m_Builder; + +public: + explicit StringBuilderAppender(StringBuilderBase& Builder) : m_Builder(&Builder) {} + + StringBuilderAppender& operator=(char C) + { + m_Builder->Append(C); + return *this; + } + StringBuilderAppender& operator*() { return *this; } + StringBuilderAppender& operator++() { return *this; } + StringBuilderAppender operator++(int) { return *this; } +}; + template<size_t N> class StringBuilder : public StringBuilderBase { diff --git a/src/zencore/string.cpp b/src/zencore/string.cpp index df7d71250..44f78aa75 100644 --- a/src/zencore/string.cpp +++ b/src/zencore/string.cpp @@ -543,6 +543,40 @@ template class StringBuilderImpl<wchar_t>; ////////////////////////////////////////////////////////////////////////// void +StringBuilderBase::AppendPaddedInt(int64_t Value, int MinWidth) +{ + char Buf[24]; + char* End = Buf + sizeof(Buf); + char* Ptr = End; + bool Negative = Value < 0; + uint64_t Abs = Negative ? uint64_t(-Value) : uint64_t(Value); + do + { + *--Ptr = '0' + char(Abs % 10); + Abs /= 10; + } while (Abs > 0); + while ((End - Ptr) < MinWidth) + { + *--Ptr = '0'; + } + if (Negative) + { + *--Ptr = '-'; + } + AppendRange(Ptr, End); +} + +void +StringBuilderBase::AppendFill(char C, size_t Count) +{ + EnsureCapacity(Count); + std::memset(m_CurPos, C, Count); + m_CurPos += Count; +} + +////////////////////////////////////////////////////////////////////////// + +void UrlDecode(std::string_view InUrl, StringBuilderBase& OutUrl) { std::string_view::size_type i = 0; |