Use virtual memory for the console buffer #14609

Closed
opened 2026-01-31 04:14:49 +00:00 by claunia · 3 comments
Owner

Originally created by @lhecker on GitHub (Jul 20, 2021).

The console buffer is currently implemented as something like this (simplified, source):

struct CharRow {
    small_vector<wchar_t, 120> chars;
};

struct TextBuffer {
    std::vector<CharRow> rows;
};

void TextBuffer::TextBuffer(int width = 120, int height = 9001) {
    rows.resize(height);

    for (const auto& row : rows) {
        row.chars.resize(width);
    }
}

This works reasonably well if your terminal is 120 characters wide, as the small_vector will prevent any heap allocations up to that size. But if your terminal is any larger it will allocate every row separately on the heap.
Additionally the use of C++ container classes force any memory to be initialized eagerly, which pessimisifies memory usage for large allocations, as all affected virtual memory is eagerly forced to be paged into physical memory.
Finally the large structure size of ROW causes instance constructions and copies, as well as iterations through the rows vector to be very costly, even if just data is read that's unrelated to the characters in the buffer (-25% performance impact).

Proposed technical implementation details

Allocate a large backing buffer for the entire TextBuffer using VirtualAlloc. Each CharRow will then only hold a begin/end pointer into the large buffer. This reduces the structure size and improves buffer performance by +20%.
Most importantly the memory used by a 160x48 conhost process is reduced from 12MB down to just 3MB (-75%) after startup.

Originally created by @lhecker on GitHub (Jul 20, 2021). The console buffer is currently implemented as something like this (simplified, [source](https://github.com/microsoft/terminal/blob/9c1331ab2ec517e832cf8fd4e0c88275103acc9f/src/buffer/out/textBuffer.cpp#L30-L52)): ```cpp struct CharRow { small_vector<wchar_t, 120> chars; }; struct TextBuffer { std::vector<CharRow> rows; }; void TextBuffer::TextBuffer(int width = 120, int height = 9001) { rows.resize(height); for (const auto& row : rows) { row.chars.resize(width); } } ``` This works reasonably well if your terminal is 120 characters wide, as the `small_vector` will prevent any heap allocations up to that size. But if your terminal is any larger it will allocate every row separately on the heap. Additionally the use of C++ container classes force any memory to be initialized eagerly, which pessimisifies memory usage for large allocations, as all affected virtual memory is eagerly forced to be paged into physical memory. Finally the large structure size of `ROW` causes instance constructions and copies, as well as iterations through the `rows` vector to be very costly, even if just data is read that's unrelated to the characters in the buffer (-25% performance impact). # Proposed technical implementation details Allocate a large backing buffer for the entire `TextBuffer` using `VirtualAlloc`. Each `CharRow` will then only hold a begin/end pointer into the large buffer. This reduces the structure size and improves buffer performance by +20%. Most importantly the memory used by a 160x48 conhost process is reduced from 12MB down to just 3MB (-75%) after startup.
claunia added the Product-ConhostArea-OutputIssue-TaskNeeds-Tag-Fix labels 2026-01-31 04:14:49 +00:00
Author
Owner

@zadjii-msft commented on GitHub (Jul 20, 2021):

Allocate a large backing buffer for the entire TextBuffer using VirtualAlloc. Each CharRow will then only hold a begin/end pointer into the large buffer. This reduces the structure size and improves buffer performance by +20%.
Most importantly the memory used by a 160x48 conhost process is reduced from 12MB down to just 3MB (-75%) after startup.

we've really come full circle, haven't we? IIRC, that's pretty similar to how the conhost buffer was designed at the start.

@zadjii-msft commented on GitHub (Jul 20, 2021): > Allocate a large backing buffer for the entire TextBuffer using VirtualAlloc. Each CharRow will then only hold a begin/end pointer into the large buffer. This reduces the structure size and improves buffer performance by +20%. > Most importantly the memory used by a 160x48 conhost process is reduced from 12MB down to just 3MB (-75%) after startup. we've really come full circle, haven't we? IIRC, that's pretty similar to how the conhost buffer was designed at the start.
Author
Owner

@DHowett commented on GitHub (Jul 20, 2021):

We will need to design this carefully to ensure that it works with the new column counting design in #8000, as well as can fault rows out to larger storage if they need to violate the implied "one wchar_t per column" invariant.

I'd rather use something a little less efficient but a little more "hands-off" like pmr and memory resources instead of having to come up with a defragmenting allocator to reign over a single block of memory 😉

@DHowett commented on GitHub (Jul 20, 2021): We will need to design this carefully to ensure that it works with the new column counting design in #8000, as well as can fault rows out to larger storage if they need to violate the implied "one `wchar_t` per column" invariant. I'd rather use something a little less efficient but a little more "hands-off" like pmr and memory resources instead of having to come up with a defragmenting allocator to reign over a single block of memory :wink:
Author
Owner

@lhecker commented on GitHub (Apr 29, 2023):

This was fixed in #13626

@lhecker commented on GitHub (Apr 29, 2023): This was fixed in #13626
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: starred/terminal#14609