C++
performance optimization
string conversion
char pointers
programming techniques

Optimizing several million char to string conversions

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

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

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.