Reduce heap fragmentation

This commit is contained in:
jpirnay
2026-05-04 17:17:38 +02:00
parent 42a9c09e4c
commit ea0f6eaa9e
3 changed files with 128 additions and 47 deletions
+76 -14
View File
@@ -1,5 +1,6 @@
#pragma once
#include <cctype>
#include <string>
#include <unordered_map>
#include <vector>
@@ -23,20 +24,38 @@ struct StackState {
int skipDepth = -1;
size_t totalTextBytes = 0;
std::vector<StackNode> stack;
// Sibling-name → count map per parent depth. Index `d` holds the counts for
// children that live at depth `d` in the DOM (i.e. queried just before
// pushing a new node). Entries are cleared lazily on push rather than popped
// and reallocated, so the per-element heap churn stays bounded.
std::vector<std::unordered_map<std::string, int>> siblingCounters;
StackState() { siblingCounters.emplace_back(); }
StackState() {
// Pre-size for typical EPUB chapter nesting (well below 32 levels). Avoids
// per-element vector growth that would otherwise interleave with map node
// allocations.
stack.reserve(32);
siblingCounters.resize(32);
}
void pushElement(const XML_Char* rawName) {
std::string name = toLowerStr(rawName ? rawName : "");
const size_t depth = stack.size();
if (siblingCounters.size() <= depth) {
siblingCounters.resize(depth + 1);
}
const int sibIdx = ++siblingCounters[depth][name];
stack.push_back({name, sibIdx, false});
siblingCounters.emplace_back();
if (skipDepth < 0 && isSkippableTag(name)) {
// Lowercase the tag in place into the StackNode's own storage — the prior
// implementation called toLowerStr() which returned a fresh std::string
// per element, a major fragmentation source. Lookup into the parent's
// sibling counter map then uses the stable in-place string with no extra
// allocation.
StackNode& node = stack.emplace_back();
node.tag.assign(rawName ? rawName : "");
for (char& c : node.tag) {
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
}
const int sibIdx = ++siblingCounters[depth][node.tag];
node.index = sibIdx;
if (skipDepth < 0 && isSkippableTag(node.tag)) {
skipDepth = static_cast<int>(stack.size()) - 1;
}
}
@@ -48,10 +67,15 @@ struct StackState {
if (skipDepth == static_cast<int>(stack.size()) - 1) {
skipDepth = -1;
}
stack.pop_back();
if (!siblingCounters.empty()) {
siblingCounters.pop_back();
// Clear the just-departed element's child-counter slot in place rather
// than freeing the map: the next sibling at this depth needs an empty map
// either way, and reusing the existing buckets avoids per-pop allocator
// churn. We don't shrink siblingCounters for the same reason.
const size_t childDepth = stack.size();
if (childDepth < siblingCounters.size()) {
siblingCounters[childDepth].clear();
}
stack.pop_back();
}
void onCharData(const XML_Char*, int) {}
@@ -67,19 +91,57 @@ struct StackState {
bool insideBody() const { return bodyIdx() >= 0; }
std::string currentXPath(const int spineIndex) const {
// Out-parameter form: appends the path into `out` without freeing it first
// so the caller controls when to reuse vs reset capacity. Use this in hot
// paths to amortise the underlying allocation.
void buildCurrentXPath(const int spineIndex, std::string& out) const {
out.clear();
out.append("/body/DocFragment[");
appendInt(out, spineIndex + 1);
out.append("]/body");
const int bi = bodyIdx();
std::string xpath = "/body/DocFragment[" + std::to_string(spineIndex + 1) + "]/body";
if (bi < 0) {
return xpath;
return;
}
for (size_t i = static_cast<size_t>(bi + 1); i < stack.size(); i++) {
xpath += "/" + stack[i].tag + "[" + std::to_string(stack[i].index) + "]";
out.push_back('/');
out.append(stack[i].tag);
out.push_back('[');
appendInt(out, stack[i].index);
out.push_back(']');
}
return xpath;
}
std::string currentXPath(const int spineIndex) const {
std::string out;
buildCurrentXPath(spineIndex, out);
return out;
}
bool shouldSkipText(const int len) const { return skipDepth >= 0 || len <= 0 || !insideBody(); }
private:
// Appends a non-negative int as decimal digits without allocating a temp
// std::string (std::to_string would allocate per call).
static void appendInt(std::string& out, int value) {
if (value < 0) {
out.push_back('-');
value = -value;
}
char buf[12];
int len = 0;
if (value == 0) {
buf[len++] = '0';
} else {
while (value > 0) {
buf[len++] = static_cast<char>('0' + (value % 10));
value /= 10;
}
}
while (len-- > 0) {
out.push_back(buf[len]);
}
}
};
template <typename StateT>