C++
performance optimization
string conversion
char pointers
programming techniques

Optimizing several million char to string conversions

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Converting millions of char* C-strings to std::string in C++ is expensive because each conversion allocates heap memory, copies characters, and may trigger the allocator frequently. The main optimizations are: pre-allocating with reserve(), using std::string_view to avoid copies entirely, employing a memory pool or arena allocator, enabling Small String Optimization (SSO), and batching conversions to reduce allocator pressure. For read-only access, std::string_view eliminates the conversion cost completely.

Baseline: Naive Conversion

cpp
1#include <string>
2#include <vector>
3
4// Slow: each conversion allocates memory and copies
5std::vector<std::string> convert(const std::vector<const char*>& inputs) {
6    std::vector<std::string> results;
7    for (const char* s : inputs) {
8        results.push_back(std::string(s));  // alloc + copy per string
9    }
10    return results;
11}
12// For 10 million strings: ~2-3 seconds, millions of allocations

Each std::string construction from char* calls strlen() to find the length, allocates heap memory (if above SSO threshold), and copies the characters.

Optimization 1: Reserve Vector Capacity

cpp
1std::vector<std::string> convert(const std::vector<const char*>& inputs) {
2    std::vector<std::string> results;
3    results.reserve(inputs.size());  // one allocation for the vector
4    for (const char* s : inputs) {
5        results.emplace_back(s);  // construct in place, no temp
6    }
7    return results;
8}
9// ~20-30% faster: avoids vector reallocation and moves

reserve() pre-allocates the vector's internal buffer. emplace_back constructs the string directly in the vector, avoiding a temporary std::string and move.

Optimization 2: Use string_view (Zero-Copy)

cpp
1#include <string_view>
2#include <vector>
3
4// No conversion at all — just wraps the existing char*
5std::vector<std::string_view> wrap(const std::vector<const char*>& inputs) {
6    std::vector<std::string_view> results;
7    results.reserve(inputs.size());
8    for (const char* s : inputs) {
9        results.emplace_back(s);  // pointer + length, no copy
10    }
11    return results;
12}
13// ~10x faster: no heap allocations, no copies

std::string_view stores a pointer and length without owning the data. It is the fastest option when you only need read access and the original char* data outlives the string_view.

Optimization 3: Provide Length to Avoid strlen

cpp
1struct RawString {
2    const char* data;
3    size_t length;
4};
5
6std::vector<std::string> convert(const std::vector<RawString>& inputs) {
7    std::vector<std::string> results;
8    results.reserve(inputs.size());
9    for (const auto& rs : inputs) {
10        results.emplace_back(rs.data, rs.length);  // skips strlen()
11    }
12    return results;
13}

If you already know the string lengths (e.g., from a database driver or parser), pass them to the std::string constructor to avoid the O(n) strlen() call per string.

Optimization 4: Arena Allocator

cpp
1#include <memory_resource>
2#include <vector>
3#include <string>
4
5// Use a monotonic buffer for all string allocations
6std::vector<std::pmr::string> convert_with_arena(
7    const std::vector<const char*>& inputs
8) {
9    // Pre-allocate a large buffer
10    std::array<char, 64 * 1024 * 1024> buffer;  // 64 MB
11    std::pmr::monotonic_buffer_resource arena(buffer.data(), buffer.size());
12
13    std::vector<std::pmr::string> results(&arena);
14    results.reserve(inputs.size());
15    for (const char* s : inputs) {
16        results.emplace_back(s, &arena);
17    }
18    return results;
19    // All memory freed at once when arena goes out of scope
20}
21// ~3-5x faster: one large allocation instead of millions of small ones

std::pmr::monotonic_buffer_resource allocates from a contiguous block without per-string malloc/free overhead. Deallocation is instant — the entire arena is freed at once.

Optimization 5: Leverage Small String Optimization (SSO)

cpp
1// Most std::string implementations use SSO:
2// Strings shorter than ~15-22 bytes (implementation-dependent)
3// are stored inline — no heap allocation
4
5// Check your compiler's SSO threshold
6#include <iostream>
7int main() {
8    std::string s;
9    // If data() points inside the string object itself, SSO is active
10    std::cout << "String object size: " << sizeof(s) << std::endl;
11    // Typically 32 bytes (GCC/Clang), 40 bytes (MSVC)
12    // SSO threshold: sizeof(string) - 1 - overhead ≈ 15-22 chars
13}
14
15// If most of your strings are short (< 15 chars),
16// the naive conversion is already fast because no heap allocation occurs

For strings below the SSO threshold, std::string stores characters in the object itself. If most of your data consists of short strings (names, IDs, codes), SSO eliminates heap allocation automatically.

Optimization 6: Parallel Conversion

cpp
1#include <algorithm>
2#include <execution>
3#include <vector>
4#include <string>
5
6std::vector<std::string> convert_parallel(const std::vector<const char*>& inputs) {
7    std::vector<std::string> results(inputs.size());
8
9    std::transform(
10        std::execution::par_unseq,
11        inputs.begin(), inputs.end(),
12        results.begin(),
13        [](const char* s) { return std::string(s); }
14    );
15
16    return results;
17}
18// ~2-4x faster on multi-core machines for large datasets

C++17 parallel algorithms distribute the conversion across multiple threads. Pre-size the output vector to avoid race conditions on push_back.

Benchmarking

cpp
1#include <chrono>
2#include <iostream>
3
4auto start = std::chrono::high_resolution_clock::now();
5
6// ... conversion code ...
7
8auto end = std::chrono::high_resolution_clock::now();
9auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(end - start).count();
10std::cout << "Converted " << count << " strings in " << ms << "ms\n";
11std::cout << "Rate: " << (count * 1000.0 / ms) << " strings/sec\n";

Common Pitfalls

  • Using std::string_view when the source char* is freed: string_view does not own the data. If the original buffer is freed or overwritten, the string_view becomes a dangling reference. Only use it when the source data outlives all views.
  • Not reserving vector capacity: Without reserve(), the vector doubles its allocation multiple times as it grows, copying all existing strings each time. For 10 million strings, this causes ~23 reallocations and copies.
  • Constructing std::string from char* without known length: The default constructor calls strlen() which scans the entire string. If you already know the length from parsing or a database API, pass it explicitly: std::string(ptr, len).
  • Allocating each string individually in a hot loop: Millions of small malloc/new calls fragment the heap and stress the allocator. Use an arena allocator (pmr::monotonic_buffer_resource) to allocate from a single contiguous block.
  • Ignoring SSO for short strings: If your strings are under ~15 characters, they are already stored inline without heap allocation. Optimizing further (arena, pool) adds complexity without meaningful speedup for short string workloads.

Summary

  • Use std::string_view for zero-copy read-only access when the source data remains valid
  • Pre-allocate with reserve() and use emplace_back to avoid vector reallocations and temporaries
  • Pass known string lengths to the std::string(ptr, len) constructor to skip strlen()
  • Use std::pmr::monotonic_buffer_resource for arena allocation when converting millions of strings
  • Strings under ~15 characters benefit from SSO — no heap allocation needed
  • Use C++17 parallel algorithms (std::execution::par) for multi-threaded conversion on large datasets

Course illustration
Course illustration

All Rights Reserved.