Skip to content

perf: optimize httpparser with C++23 string_view, ispanstream, and transparent maps#203

Draft
clauspruefer with Copilot wants to merge 4 commits into
mainfrom
copilot/optimize-httpparser-cpp
Draft

perf: optimize httpparser with C++23 string_view, ispanstream, and transparent maps#203
clauspruefer with Copilot wants to merge 4 commits into
mainfrom
copilot/optimize-httpparser-cpp

Conversation

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor

The HTTP parser made unnecessary heap allocations throughout parsing — destructive string splits, full copies for URL substrings, intermediate vector<string> for header lines, and map lookups that constructed temporary std::string keys.

C++ standard

  • Bumped -std=c++20-std=c++23 in CMakeLists.txt to unlock <spanstream>

httpparser.hpp

  • Transparent map lookup: Added StringHash with is_transparent; RequestHeader_t and URLParamMap_t now use unordered_map<string, string, StringHash, equal_to<>>.find() / .at() on string literals no longer constructs a temporary std::string
  • Non-destructive split overload: split(string_view, string_view, vector<string_view>&) slices into the original buffer with zero per-token heap allocation; the existing destructive split(string&, …) is retained for _HTTPRequestBuffer management
  • rsplit / is_digits: Parameters narrowed from string& / const string& to string_view
  • Parse method signatures: _parseRequestProperties, _parseRequestHeaders, _parseGETParameter all accept string_view instead of string& / const string&
  • getRequests(): Returns const RequestsMap_t& instead of a by-value copy

httpparser.cpp

  • _parseRequestHeaders: Replaced the destructive split + rsplit loop (which allocated a vector<string> and mutated the request string in place) with std::ispanstream line-by-line parsing; key and value are string_view slices — the only allocations are the final emplace into the map
// Before: intermediate vector<string>, mutates Request in place
vector<string> Lines;
StringHelper::split(Request, "\r\n", Lines);
Lines.push_back(Request);
for (auto &Line : Lines) { /* rsplit into HeaderPair, emplace */ }

// After: no intermediate container, no mutation
ispanstream ss(span<const char>(Request.data(), Request.size()));
string line;
while (getline(ss, line, '\n')) {
    if (!line.empty() && line.back() == '\r') line.pop_back();
    const size_t colonSpace = line.find(": ");
    if (colonSpace != string::npos && colonSpace > 0) {
        string_view lv(line);
        ResultRef.emplace(string(lv.substr(0, colonSpace)), string(lv.substr(colonSpace + 2)));
    }
}
  • _parseGETParameter: URLParamsPart is now a string_view into the URL (no substr copy); uses the new non-destructive split into vector<string_view>
  • erase over replace: Replaced replace(0, N, "") with erase(0, N) at both POST payload consumption sites

Copilot AI added 2 commits July 27, 2026 08:46
…arent maps

- Bump C++ standard from c++20 to c++23 (enables <spanstream>)
- httpparser.hpp:
  - Add StringHash with is_transparent for zero-copy heterogeneous map lookup
  - Replace typedef with using for RequestHeader_t, URLParamMap_t (now use StringHash + equal_to<>)
  - Add non-destructive split(string_view, string_view, vector<string_view>&) overload
  - Destructive split(string&, ...) kept for _HTTPRequestBuffer management
  - rsplit: change first param string& -> string_view (avoids copy at call site)
  - is_digits: change param const string& -> string_view
  - _parseRequestProperties: change string& -> string_view
  - _parseRequestHeaders: change string& -> string_view
  - _parseGETParameter: change const string& -> string_view
  - getRequests(): return const RequestsMap_t& instead of value copy
- httpparser.cpp:
  - #include <spanstream>
  - _parseRequestHeaders: replace vector<string>/split/rsplit loop with ispanstream
    line-by-line parse; no intermediate string vector; uses string_view key/value slices
  - _parseGETParameter: URLParamsPart now string_view (no heap copy of URL suffix);
    uses non-destructive split into vector<string_view>; removes manual push_back
  - Replace replace(0, N, "") with erase(0, N) in two POST payload sites
  - Remove redundant .substr(0, length()) in header value emplace
- Use const auto& (not auto) for string_view loop variable in _parseGETParameter
- Add clarifying safety comments to rsplit underflow guards

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because there is no eligible user to bill. To allow Copilot reviews on bot-authored pull requests, enable direct organization billing in your organization's Copilot settings.

@clauspruefer clauspruefer left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

@copilot Looks good on the first sight. But please add the following tests and functionality: a) performance test / comparison to old parsing behaviour, use apx. 500 random generated valid HTTP requests containing different header counts / payload sizes, repeat the tests 500 times and save the results (including comparison data) as CSV b) do the same as in performance tests (a) but measure the used memory and also save the results as CSV

- test/performance/http-parser/test-parser-performance.cpp:
  500 random HTTP requests × 500 iterations; times new (ispanstream /
  string_view) vs legacy (vector<string> / rsplit) header and GET-param
  parsing; writes results-performance.csv with per-request avg/min/max ns
  and speedup columns.  Observed 3.3x average header-parse speedup.

- test/performance/http-parser/test-parser-memory.cpp:
  Same 500 requests; overrides global operator new to count gross bytes
  allocated during each parse; writes results-memory.csv with per-request
  alloc count / bytes and reduction-% columns.  Observed 72% byte reduction
  for header parsing.

- test/performance/http-parser/CMakeLists.txt: build targets for both
  standalone executables (no Boost dependency).

- test/performance/CMakeLists.txt: add_subdirectory(http-parser).
Copilot AI review requested due to automatic review settings July 27, 2026 09:09

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because there is no eligible user to bill. To allow Copilot reviews on bot-authored pull requests, enable direct organization billing in your organization's Copilot settings.

- Use chrono::nanoseconds::rep for Nanos type alias
- Replace C-style sizeof casts with static_cast<int>
- Add thread-safety comment to global tracking variables
- Fix median() for even-length vectors (average two central elements)
Copilot AI review requested due to automatic review settings July 27, 2026 09:11

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Copilot was unable to review this pull request because there is no eligible user to bill. To allow Copilot reviews on bot-authored pull requests, enable direct organization billing in your organization's Copilot settings.

Copilot AI commented Jul 27, 2026

Copy link
Copy Markdown
Contributor Author

@copilot Looks good on the first sight. But please add the following tests and functionality: a) performance test / comparison to old parsing behaviour, use apx. 500 random generated valid HTTP reques...

Added in commit a8e7b9d (two commits: test: add HTTP parser performance and memory benchmark tests + test: address code review findings in benchmark tests).

test/performance/http-parser/test-parser-performance.cpp
Generates 500 random valid HTTP requests (varying header counts 1–15, GET-param counts 0–5, POST payloads 0–2 048 B, mixed GET/POST) with a fixed seed. Each request is parsed 500 times by both implementations and per-request avg/min/max nanoseconds plus speedup are written to results-performance.csv (19 columns, 500 rows). Observed summary on this machine:

Phase New (total µs) Legacy (total µs) Speedup
Header parsing 449 1 376 3.06×
GET-param parsing 54 68 1.24×

test/performance/http-parser/test-parser-memory.cpp
Same 500 requests; replaces global ::operator new / ::operator delete with tracking wrappers to count gross bytes allocated per parse call. Median over 10 measurements per request is written to results-memory.csv (17 columns, 500 rows). Observed summary:

Phase New (alloc bytes) Legacy (alloc bytes) Reduction
Header parsing 503 661 1 813 194 72.2 %
GET-param parsing 89 832 122 038 26.4 %

Both executables are standalone (no Boost dependency) and are wired into the CMake build via test/performance/CMakeLists.txtadd_subdirectory(http-parser). The CSV path can be overridden via a CLI argument (./test-parser-performance my-results.csv).

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.

3 participants