aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorStefan Boberg <[email protected]>2026-04-16 14:22:09 +0200
committerGitHub Enterprise <[email protected]>2026-04-16 14:22:09 +0200
commitdb616a2ede14670423b7b2b28aa36c33cabb030e (patch)
treeced9c440544f12961207a0465590b6df3d0609e9
parent5.8.5-pre1 (diff)
downloadarchived-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.md35
-rw-r--r--src/zen/cmds/wipe_cmd.cpp1
-rw-r--r--src/zencore/include/zencore/fmtutils.h43
-rw-r--r--src/zencore/include/zencore/hashutils.h19
-rw-r--r--src/zencore/include/zencore/iohash.h1
-rw-r--r--src/zencore/include/zencore/string.h24
-rw-r--r--src/zencore/string.cpp34
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;