Document Number: | PXXXXR0 |
Date: | 2024/03/04 |
Reply-to: | cpp@kaotic.software |
Authors: | Tiago Freire |
Audience: | LWG |
C++26
Addition of sizing functions complementing to_chars
# | Description |
---|---|
0 | Initial draft |
1. Motivation
2. Prior Art
3. Naming
4. Wording
5. References
std::to_chars has added an enormous value in both consistency and performance improvements in the way numbers are converted into strings.
A user can pass in the number that they want to format (with optional formatting parameters) and a pointer to a buffer.
But how many bytes does the buffer need to have? The answer to this is not obvious to a user, nor is the solution as trivial as it might look at first glance.
The first objection to this is that a buffer upper bound can be known in advance; 3, 4, 5, 6, 10, 11, or 20 bytes for integer types, 15 or 24 bytes for floats and doubles in round-trip shortest,
and other user determined by format specification.
But you rarely ever need the maximum size for a given type to be able to format a number expressed in such type, and that number can go as low as 1byte.
Why not just allocate an oversized array on the stack and shorten it after formatting when the actual size is known?
Consider instead of a single number conversion we are converting a vector of numbers of upwards to 30 elements all in the same contiguous buffer, what can a developer do in this situation?
They can over allocate a buffer, which for a random set will be on average 2× as large as need, but can be up to 24× as large as necessary.
But what if you want to avoid over allocation?
Then they can perform a conversion on the stack and:
This paper proposes the introduction of sizing functions that calculates the required buffer size without conversion, which can be done more cheaply than converting and discarding the value.
Allowing, in the previous example, for the required size to be cheaply pre-calculated, avoid over-allocation, and complete a complex conversion with reduced waste.
This paper comes in the wake of a successful implementation of equivalent functions in a public available library.
Functions for size estimation are on average 2.5× times faster than the conversion itself.
Leading to an on average time reduction between 12% to 60% over the double-pass conversion algorithm.
The following are benchmarks for converting an array of 128 numbers of differing types with all optimizations enabled, time of conversion measured in nano seconds.
The percentages indicate a time difference from the size+pass algorithm that can be achieved using to_chars_size, bigger number is worse.
MSVC x86_64:
Type | to_chars_size | size + pass | double pass | reallocation | over allocation |
---|---|---|---|---|---|
uint8_t | 76 | 272 | 329 (+21%) | 973 (+258%) | 227 (-17%) |
uint16_t | 113 | 355 | 483 (+36%) | 1086 (+206%) | 355 ( 0%) |
uint32_t | 194 | 582 | 769 (+32%) | 1294 (+122%) | 496 (-15%) |
uint64_t | 286 | 977 | 1391 (+42%) | 1659 (+ 70%) | 799 (-18%) |
int8_t | 80 | 290 | 395 (+36%) | 1019 (+251%) | 250 (-14%) |
int16_t | 105 | 390 | 570 (+46%) | 1144 (+193%) | 316 (-19%) |
int32_t | 208 | 689 | 815 (+18%) | 1292 (+ 88%) | 478 (-31%) |
int64_t | 325 | 1093 | 1471 (+35%) | 1674 (+ 53%) | 778 (-29%) |
float (round trip shortest) | 1971 | 4755 | 5429 (+14%) | 3505 (- 26%) | 2701 (-43%) |
double (round trip shortest) | 3423 | 7999 | 9172 (+15%) | 5307 (- 34%) | 4770 (-40%) |
Type | to_chars_size | size + pass | double pass | reallocation | over allocation |
---|---|---|---|---|---|
uint8_t | 11 | 94 | 140 (+49%) | 512 (+445%) | 94 ( 0%) |
uint16_t | 83 | 255 | 306 (+20%) | 589 (+131%) | 164 (-36%) |
uint32_t | 84 | 334 | 452 (+35%) | 690 (+107%) | 247 (-26%) |
uint64_t | 142 | 660 | 1048 (+59%) | 1023 (+ 55%) | 548 (-17%) |
int8_t | 77 | 169 | 197 (+17%) | 765 (+353%) | 117 (-31%) |
int16_t | 56 | 231 | 326 (+41%) | 824 (+257%) | 172 (-26%) |
int32_t | 72 | 327 | 516 (+58%) | 901 (+176%) | 270 (-17%) |
int64_t | 108 | 633 | 1014 (+60%) | 1185 (+ 87%) | 542 (-14%) |
float (round trip shortest) | 1328 | 3055 | 3428 (+12%) | 2191 (- 28%) | 1773 (-42%) |
double (round trip shortest) | 2857 | 6399 | 7231 (+13%) | 4102 (- 36%) | 3626 (-43%) |
I suggest the usage of the name "to_chars_size" to closely represent the relationship between the to_chars facility and its size only counterpart.
In subclause 22.13.1 [charconv.syn], add the following as indicated:
to_chars_result to_chars(char* first, char* last, // freestanding-deleted floating-point-type value, chars_format fmt, int precision);
constexpr size_t to_chars_size(integer-type value, int base = 10); // freestanding size_t to_chars_size(floating-point-type value); // freestanding size_t to_chars_size(floating-point-type value, chars_format fmt); // freestanding size_t to_chars_size(floating-point-type value, chars_format fmt, int precision); // freestanding
In subclause 22.13.2 [charconv.to.chars], add the following as indicated:
The functions taking a chars_format parameter determine the conversion specifier for printf as follows: The conversion specifier is f if fmt is chars_format::fixed, e if fmt is chars_format::scientific, a (without leading "0x" in the result) if fmt is chars_format::hex, and g if fmt is chars_format::general.
All functions named to_chars_size, return the minimum size of the buffer required to receive a successful conversion of their to_chars counterparts.
In the same subclause also add:
to_chars_result to_chars(char* first, char* last, floating-point-type value, chars_format fmt, int precision); Preconditions: fmt has the value of one of the enumerators of chars_format. Effects: value is converted to a string in the style of printf in the "C" locale with the given precision. Throws: Nothing.
constexpr size_t to_chars_size(integer-type value, int base = 10); Preconditions: base has a value between 2 and 36 (inclusive). Throws: Nothing. size_t to_chars_size(floating-point-type value); Throws: Nothing. size_t to_chars_size(floating-point-type value, chars_format fmt); Preconditions: fmt has the value of one of the enumerators of chars_format. Throws: Nothing. size_t to_chars_size(floating-point-type value, chars_format fmt, int precision); Preconditions: fmt has the value of one of the enumerators of chars_format. Throws: Nothing.