to_chars_size

Document Number:PXXXXR0
Date:2024/03/04
Reply-to:cpp@kaotic.software
Authors:Tiago Freire
Audience:LWG

Target

C++26

Abstract

Addition of sizing functions complementing to_chars

Revision

#Description
0Initial draft

Table of Contents

1. Motivation
2. Prior Art
3. Naming
4. Wording
5. References

Motivation

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:

  1. resize a heap allocated destination buffer (per item), and copy the previously converted buffer from the stack
  2. discard the conversion, keeping only the size for counting, allocate once, and then re-doing the conversion from the beginning
But is this the best we can do, either over-allocate or waste time?

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.

Prior Art

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 passreallocation 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%)

gcc x86_64:
Type to_chars_size size + pass double passreallocation 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%)

This paper doesn't claim that all algorithms can be made faster as a consequence of having a "to_chars_size", but it allows for a better time trade-off when memory usage is constrained. In addition, it complements the feature by allowing developers to answer the question "how much buffer do I need" before (or independently from) performing the conversion.

Naming

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.

Wording

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.
		

References

  1. CoreLib size pre-calculation