Copyright(c) 2019-2020 Leonid Yuriev leo@yuriev.ru, Copyright(c) 2014 Milo Yip miloyip@gmail.com
Due to anonymity requirements, the author email address is not listed.
This fork of the benchmark was created to demonstrate the performance superiority of my new dtoa() implementation over others.
All the source code can be found in the src/d2sci directory.
Because the number of double values is too large to be fully tested, there may be bugs. If there is any problem with the running results, please give feedback. There may be clerical errors in the relevant notes, please understand.
This benchmark evaluates the performance of conversion from double precision IEEE-754 floating point (double) to ASCII string. The function prototype is:
void dtoa(double value, char* buffer);The character string result must be convertible to the original value exactly via some correct implementation of strtod(), i.e. roundtrip convertible.
Note that dtoa() is not a standard function in C and C++.
All new APIs are as follows:
extern "C"{
int d2sci_sse(double value, char *buffer);// print 1 double to 1 buffer ; digit 17;sse instruction
int d2sci_avx512(double value,char* buffer);// print 1 double to 1 buffer ; digit 17;avx512 instruction
int d2sci_avx512_lut(double value,char* buffer);// print 1 double to 1 buffer ; digit 17;avx512 instruction + look up table
int d2sci_lut(double value,char* buffer);// print 1 double to 1 buffer ; digit 17;look up table
int d2sci(double value,char* buffer);// same as d2sci_avx512_lut
//performance
//d2sci = d2sci_avx512_lut > d2sci_avx512 > d2sci_sse > d2sci_lut
int d2sci_32(double* value,char* buffer);// print 32 double to 1 buffer ; digit 17 ; avx512 instruction
int d2sci_32_init();//
void d2sci_32v(double* value,char** buffer);// print 32 double to 32 buffer ; digit 17 ; avx512 instruction
void d2s_32v(double* value,char** buffer);// print 32 double to 32 buffer ; shortest significant result;avx512 instruction
int d2s_avx512(double value, char *buffer);//print 1 double to 1 buffer ; shortest significant result;avx512 instruction
int d2s_sse(double value, char *buffer);//print 1 double to 1 buffer ; shortest significant result;sse instruction
}Some API need CPU support AVX512 instruction set.
- Print 1 double values to 1 buffers. The print result of double value contains 17 digits. return write buffer length; All functions have the same output.
int d2sci (double value, char* buffer);//need avx512 instruction set,same as d2sci_avx512_lut
int d2sci_sse (double value, char* buffer);//need sse instruction set
int d2sci_avx512 (double value, char* buffer);//need avx512 instruction set
int d2sci_avx512_lut(double value, char* buffer);//need avx512 instruction set
int d2sci_lut (double value, char* buffer);//use more look up table//example
double v=1.3;
char buf[32];
int len = d2sci(v,buf);
//int len = d2sci_sse(v,buf);
printf("len=%d,%s",len,buf);//len=22,1.3000000000000000e+00
//special value: when v is 1e-323 print 0.9881312916824931e-323- Continuously print 32 double values to the buffer. The print result of each double value contains 17 digits. return write buffer length;
int d2sci_32(double* value, char* buffer);//example
double v32[32];
for(int i=0;i<32;++i) v32[i] = (i+1)*sin(1);
char buffer_1024[1024];
int len = d2sci_32(v32,buffer_1024);
printf("len=%d\n%s",len,buffer_1024);
//print result
len=768
8.4147098480789650e-01
1.6829419696157930e+00
2.5244129544236892e+00
3.3658839392315860e+00
4.2073549240394827e+00
5.0488259088473785e+00
5.8902968936552753e+00
6.7317678784631720e+00
7.5732388632710687e+00
8.4147098480789654e+00
9.2561808328868622e+00
1.0097651817694757e+01
1.0939122802502654e+01
1.1780593787310551e+01
1.2622064772118447e+01
1.3463535756926344e+01
1.4305006741734241e+01
1.5146477726542138e+01
1.5987948711350034e+01
1.6829419696157930e+01
1.7670890680965825e+01
1.8512361665773724e+01
1.9353832650581619e+01
2.0195303635389514e+01
2.1036774620197412e+01
2.1878245605005307e+01
2.2719716589813206e+01
2.3561187574621101e+01
2.4402658559428999e+01
2.5244129544236894e+01
2.6085600529044793e+01
2.6927071513852688e+01
//Nan or inf will not be printed when the input value is nan or inf.- Print 32 double values to 32 buffers. The print result of each double value contains 17 digits.
void d2sci_32v(double* value, char** buffer);//example
double v32[32];
for(int i=0;i<32;++i) v32[i] = (i+1)*sin(1);
char buffer_1024[1024];
char* buf_32[32];
for(int i=0;i<32;++i) buf_32[i]=&buffer_1024[i*32];
d2sci_32v(v32,buf_32);
for(int i=0;i<32;++i) printf("i=%d,%.16le %s\n",i,v32[i],buf_32[i]);
//print result
i= 0,8.4147098480789650e-01 8.4147098480789650e-01
i= 1,1.6829419696157930e+00 1.6829419696157930e+00
i= 2,2.5244129544236893e+00 2.5244129544236893e+00
i= 3,3.3658839392315860e+00 3.3658839392315860e+00
i= 4,4.2073549240394827e+00 4.2073549240394827e+00
i= 5,5.0488259088473786e+00 5.0488259088473786e+00
i= 6,5.8902968936552753e+00 5.8902968936552753e+00
i= 7,6.7317678784631720e+00 6.7317678784631720e+00
i= 8,7.5732388632710688e+00 7.5732388632710688e+00
i= 9,8.4147098480789655e+00 8.4147098480789655e+00
i=10,9.2561808328868622e+00 9.2561808328868622e+00
i=11,1.0097651817694757e+01 1.0097651817694757e+01
i=12,1.0939122802502654e+01 1.0939122802502654e+01
i=13,1.1780593787310551e+01 1.1780593787310551e+01
i=14,1.2622064772118447e+01 1.2622064772118447e+01
i=15,1.3463535756926344e+01 1.3463535756926344e+01
i=16,1.4305006741734241e+01 1.4305006741734241e+01
i=17,1.5146477726542138e+01 1.5146477726542138e+01
i=18,1.5987948711350034e+01 1.5987948711350034e+01
i=19,1.6829419696157931e+01 1.6829419696157931e+01
i=20,1.7670890680965826e+01 1.7670890680965826e+01
i=21,1.8512361665773724e+01 1.8512361665773724e+01
i=22,1.9353832650581619e+01 1.9353832650581619e+01
i=23,2.0195303635389514e+01 2.0195303635389514e+01
i=24,2.1036774620197413e+01 2.1036774620197413e+01
i=25,2.1878245605005308e+01 2.1878245605005308e+01
i=26,2.2719716589813206e+01 2.2719716589813206e+01
i=27,2.3561187574621101e+01 2.3561187574621101e+01
i=28,2.4402658559429000e+01 2.4402658559429000e+01
i=29,2.5244129544236895e+01 2.5244129544236895e+01
i=30,2.6085600529044793e+01 2.6085600529044793e+01
i=31,2.6927071513852688e+01 2.6927071513852688e+01
//Nan or inf will not be printed when the input value is nan or inf.- Print 32 double values to 32 buffers. this API base on Schubfach Algorithm;AVX-512 implementation. The print result of each double value contains the shortest significant digit.
void d2s_32v(double* value, char** buffer);//example
double v32[32];
for(int i=0;i<32;++i) v32[i] = (i+1)*sin(1);
char buffer_1024[1024];
char* buf_32[32];
for(int i=0;i<32;++i) buf_32[i]=&buffer_1024[i*32];
d2s_32v(v32,buf_32);
for(int i=0;i<32;++i) printf("i=%2d,%.17lg %s\n",i,v32[i],buf_32[i]);
//print result
i= 0,0.8414709848078965 8.414709848078965e-01
i= 1,1.682941969615793 1.682941969615793e+00
i= 2,2.5244129544236893 2.5244129544236893e+00
i= 3,3.365883939231586 3.365883939231586e+00
i= 4,4.2073549240394827 4.207354924039483e+00
i= 5,5.0488259088473786 5.048825908847379e+00
i= 6,5.8902968936552753 5.890296893655275e+00
i= 7,6.731767878463172 6.731767878463172e+00
i= 8,7.5732388632710688 7.573238863271069e+00
i= 9,8.4147098480789655 8.414709848078965e+00
i=10,9.2561808328868622 9.256180832886862e+00
i=11,10.097651817694757 1.0097651817694757e+01
i=12,10.939122802502654 1.0939122802502654e+01
i=13,11.780593787310551 1.178059378731055e+01
i=14,12.622064772118447 1.2622064772118447e+01
i=15,13.463535756926344 1.3463535756926344e+01
i=16,14.305006741734241 1.430500674173424e+01
i=17,15.146477726542138 1.5146477726542138e+01
i=18,15.987948711350034 1.5987948711350034e+01
i=19,16.829419696157931 1.682941969615793e+01
i=20,17.670890680965826 1.7670890680965826e+01
i=21,18.512361665773724 1.8512361665773724e+01
i=22,19.353832650581619 1.935383265058162e+01
i=23,20.195303635389514 2.0195303635389514e+01
i=24,21.036774620197413 2.1036774620197413e+01
i=25,21.878245605005308 2.1878245605005308e+01
i=26,22.719716589813206 2.2719716589813206e+01
i=27,23.561187574621101 2.35611875746211e+01
i=28,24.402658559429 2.4402658559429e+01
i=29,25.244129544236895 2.5244129544236895e+01
i=30,26.085600529044793 2.6085600529044793e+01
i=31,26.927071513852688 2.6927071513852688e+01- Print 1 double values to 1 buffers. this API base on Schubfach Algorithm; The print result of each double value contains the shortest significant digit. These two functions have the same output. return write buffer length;
int d2s_avx512(double value, char* buffer);//need avx512 instruction
int d2s_sse (double value, char* buffer);//need sse instruction
// To increase that readability of the result
// input double v range and print result ;
// range : double v -> print result
// 1. v<1e-7 : 1.23e-8 -> 1.23e-08 , 1.23e-11 -> 1.23e-11 , 1.23e-103 -> 1.23e-103
// 2. [1e-7,1) : 1.23e-3 -> 0.00123 , 1.23e-1 -> 0.123 , 0.0123 -> 0.0123
// 3. [1,1e9) : 1.23e2 -> 123 , 1.23e5 -> 12300 123.45 -> 123.45
// 4. >=1e9 : 1.23e10 -> 1.23e+10 , 1.23e9 -> 1.23e+09 , 1.23e103 -> 1.23e+103
// Print integer results when the absolute value of input double is an integer between 0 and 2**53-1. //example
double v[8] = {1e-123,-0.0123,1.23,123,123.45,10000,1e8,1.23e100};
char buf[32];
for(int i=0;i<8;++i){
int len = d2s_avx512(v[i],buf);//or d2s_sse(v[i],buf);
printf("len=%d,%s\n",len,buf);
}
//print result
len=6,1e-123
len=7,-0.0123
len=4,1.23
len=3,123
len=6,123.45
len=5,10000
len=9,100000000
len=9,1.23e+100Firstly the program verifies the correctness of all algorithm algorithmimplementations.
There are performance test results in three data modes.
Consecutive positive and negative integer values.
Random case for benchmark is carried out:
- Each iteration generates 2048*64=131072 random
doublevalues, filtered out+/-infandnan. - Convert these generated numbers into ASCII.
- Iterations are run 10 times taking the minimum time.
RandomDigit case for benchmark is carried out:
-
Each digit generates 2048*64=131072 random
doublevalues, filtered out+/-infandnan. Then convert them to limited precision (1 to 17 decimal digits in significand). -
Convert these generated numbers into ASCII.
-
Each digit group is run 10 times and the minimum time is taken.
[1] software environment:Linux , compiler support c++20; example : (1) ubuntu 24.04 ; (2) g++ 13.3 , icpx 2025.0.4 ,clang 18; [2] hardware environment:CPU support AVX-512 example : (1) intel xeon processor ; (2) intel 11th Gen core ; i9 11900 ; (3) AMD Zen4 or Zen5 ; AMD R7 7840H , AMD R7 7950X , AMD R9 9950X.
The steps to compile and run are as follows:
- Obtain cmake
cd dtoa-benchmarkmkdir buildcd buildcmake ..make -j./d2a-benchmark- The result appears in the running program directory.
All the source code of d2sci is compiled into a static library libd2sci2.a through icpx 2025.0.4 and clang++ 18.1.3.
The software runs in the following environment: (1)AMD R9 9950X (2)Ubuntu 24.04 (3)icpx 2025.0.4(The results compiled by this compiler have the best performance)
The running results are as follows:
| Function | Min ns | RMS ns | Max ns | Sum ns | Speedup |
|---|---|---|---|---|---|
| null | 1.065 | 1.067 | 1.071 | 18.147 | ×97.564 |
| d2sci_32 | 1.199 | 1.204 | 1.208 | 20.475 | ×86.471 |
| d2sci_32v | 1.196 | 1.208 | 1.212 | 20.529 | ×86.241 |
| d2s_32v | 2.346 | 2.364 | 2.377 | 40.186 | ×44.057 |
| d2s_avx512 | 1.775 | 2.957 | 4.322 | 48.389 | ×36.589 |
| d2s_sse | 1.931 | 3.011 | 4.200 | 49.608 | ×35.690 |
| d2sci_avx512_lut | 3.125 | 3.136 | 3.144 | 53.305 | ×33.214 |
| d2sci_avx512 | 3.542 | 3.556 | 3.566 | 60.444 | ×29.291 |
| d2sci_sse | 3.886 | 3.897 | 3.916 | 66.252 | ×26.723 |
| d2sci_lut | 4.296 | 4.309 | 4.336 | 73.252 | ×24.170 |
| erthink | 3.137 | 5.232 | 7.390 | 85.053 | ×20.816 |
| dragonbox | 4.146 | 5.223 | 6.353 | 87.837 | ×20.156 |
| schubfach | 4.188 | 5.474 | 6.751 | 91.706 | ×19.306 |
| ryu | 4.751 | 6.399 | 7.590 | 107.827 | ×16.420 |
| erthink_shodan | 6.346 | 11.594 | 16.321 | 189.279 | ×9.354 |
| grisu_exact | 10.853 | 11.440 | 11.927 | 194.369 | ×9.109 |
| emyg | 6.550 | 13.981 | 18.850 | 231.434 | ×7.650 |
| milo | 11.950 | 18.848 | 23.580 | 315.487 | ×5.612 |
| floaxie | 14.718 | 21.244 | 27.126 | 356.188 | ×4.971 |
| fmt | 14.593 | 21.349 | 25.840 | 359.679 | ×4.922 |
| to_chars | 20.117 | 23.502 | 25.372 | 398.985 | ×4.437 |
| doubleconv | 17.181 | 24.522 | 30.160 | 412.305 | ×4.294 |
| grisu2 | 17.401 | 25.041 | 29.866 | 421.024 | ×4.205 |
| fpconv | 17.485 | 30.218 | 39.755 | 499.712 | ×3.543 |
| stb | 30.737 | 34.813 | 38.485 | 590.688 | ×2.997 |
| format | 60.923 | 64.794 | 69.422 | 1100.333 | ×1.609 |
| dragon4 | 22.786 | 104.860 | 168.424 | 1615.349 | ×1.096 |
| sprintf | 64.202 | 107.486 | 153.510 | 1770.487 | ×1.000 |
| ostrstream | 123.378 | 172.399 | 225.709 | 2888.738 | ×0.613 |
| ostringstream | 115.242 | 176.904 | 264.126 | 2942.899 | ×0.602 |
The following are results measured by Random testcase.
The speedup is based on sprintf's Sum values.
Time is the average value of printing a single double value.
| Function | Sum ns | Speedup |
|---|---|---|
| null | 1.106 | ×250.285 |
| d2sci_32 | 1.244 | ×222.384 |
| d2sci_32v | 1.255 | ×220.556 |
| d2s_32v | 2.477 | ×111.741 |
| d2sci_avx512_lut | 3.296 | ×83.957 |
| d2sci_avx512 | 3.748 | ×73.845 |
| d2sci_sse | 4.099 | ×67.518 |
| d2sci_lut | 4.534 | ×61.039 |
| d2s_avx512 | 10.641 | ×26.007 |
| d2s_sse | 10.857 | ×25.491 |
| dragonbox | 18.341 | ×15.089 |
| schubfach | 20.939 | ×13.217 |
| ryu | 25.292 | ×10.942 |
| grisu_exact | 26.534 | ×10.430 |
| floaxie | 29.781 | ×9.293 |
| erthink | 30.003 | ×9.224 |
| erthink_shodan | 32.920 | ×8.407 |
| emyg | 46.102 | ×6.003 |
| grisu2 | 50.210 | ×5.512 |
| to_chars | 51.354 | ×5.389 |
| milo | 54.160 | ×5.110 |
| fmt | 55.126 | ×5.020 |
| stb | 55.964 | ×4.945 |
| fpconv | 58.811 | ×4.706 |
| doubleconv | 61.243 | ×4.519 |
| format | 89.519 | ×3.092 |
| sprintf | 276.751 | ×1.000 |
| ostrstream | 356.084 | ×0.777 |
| ostringstream | 392.724 | ×0.705 |
| dragon4 | 582.747 | ×0.475 |
The following are results measured by RandomDigit testcase.
The speedup is based on sprintf's Sum values.
Time is the average value of printing a single double value.
| Function | Min ns | RMS ns | Max ns | Sum ns | Speedup |
|---|---|---|---|---|---|
| null | 1.113 | 1.131 | 1.247 | 19.210 | ×226.579 |
| d2sci_32v | 1.266 | 1.267 | 1.269 | 21.542 | ×202.056 |
| d2sci_32 | 1.249 | 1.268 | 1.277 | 21.561 | ×201.879 |
| d2s_32v | 2.460 | 2.494 | 2.512 | 42.395 | ×102.670 |
| d2sci_avx512_lut | 3.288 | 3.304 | 3.324 | 56.161 | ×77.504 |
| d2sci_avx512 | 3.745 | 3.762 | 3.792 | 63.955 | ×68.058 |
| d2sci_sse | 4.094 | 4.108 | 4.121 | 69.840 | ×62.323 |
| d2sci_lut | 4.512 | 4.530 | 4.565 | 77.007 | ×56.523 |
| d2s_avx512 | 10.698 | 10.767 | 10.849 | 183.036 | ×23.780 |
| d2s_sse | 10.843 | 10.905 | 11.009 | 185.383 | ×23.479 |
| dragonbox | 10.998 | 13.292 | 18.586 | 224.172 | ×19.417 |
| schubfach | 12.411 | 14.578 | 19.204 | 246.487 | ×17.659 |
| floaxie | 13.546 | 22.808 | 29.610 | 378.276 | ×11.507 |
| erthink_shodan | 18.991 | 23.285 | 33.007 | 392.356 | ×11.094 |
| erthink | 19.903 | 23.970 | 30.122 | 405.033 | ×10.746 |
| grisu_exact | 24.571 | 25.343 | 26.702 | 430.728 | ×10.105 |
| ryu | 24.163 | 29.153 | 33.886 | 492.545 | ×8.837 |
| emyg | 32.307 | 40.927 | 47.269 | 693.229 | ×6.279 |
| grisu2 | 34.908 | 41.516 | 50.512 | 702.151 | ×6.199 |
| milo | 29.044 | 43.273 | 55.044 | 726.322 | ×5.993 |
| fmt | 33.700 | 43.589 | 55.147 | 736.072 | ×5.913 |
| doubleconv | 35.051 | 48.577 | 60.974 | 819.179 | ×5.313 |
| fpconv | 42.804 | 50.401 | 58.940 | 854.342 | ×5.095 |
| to_chars | 50.101 | 50.860 | 52.198 | 864.529 | ×5.035 |
| stb | 53.285 | 55.175 | 56.037 | 937.914 | ×4.641 |
| format | 88.883 | 91.972 | 94.056 | 1563.392 | ×2.784 |
| sprintf | 232.795 | 256.283 | 269.735 | 4352.681 | ×1.000 |
| ostrstream | 316.879 | 339.209 | 354.910 | 5763.944 | ×0.755 |
| dragon4 | 132.041 | 389.714 | 583.975 | 6185.312 | ×0.704 |
| ostringstream | 344.971 | 370.825 | 389.556 | 6299.091 | ×0.691 |
The following ranking results are from 2.Random test results. Because some functions perform very closely, the sorting results may not match the test results.
| Function | Description |
|---|---|
| d2sci_32v | Base on AVX-512, print 32 double to 32 buffer. |
| d2sci_32 | Base on AVX-512, print 32 double to 1 buffer. |
| d2s_32v | Base on Schubfach and AVX-512, print 32 double to 32 buffer. |
| d2sci_avx512_lut | Base on AVX-512, print 1 double to 1 buffer.(Same as d2sci) |
| d2sci_avx512 | Base on AVX-512, print 1 double to 1 buffer. |
| d2sci_sse | Base on sse, print 1 double to 1 buffer. |
| d2sci_lut | use more look up table, print 1 double to 1 buffer. |
| d2s_avx512 | Base on Schubfach and AVX-512, print 1 double to 1 buffer. |
| d2s_sse | Base on Schubfach and sse, print 1 double to 1 buffer. |
| dragonbox | Junekey Jeon's CPP implementation. |
| schubfach | Raffaello Giulietti's algorithm.Origin source code from https://github.com/c4f7fcce9cb06515/Schubfach |
| ryu | Ulf Adams's Ryū algorithm. |
| grisu_exact | Junekey Jeon's CPP implementation. |
| floaxie | Alexey Chernov's Grisu2 implementation. |
| erthink | Leonid Yuriev's Grisu-based C++ implementation. |
| erthink_shodan | Leonid Yuriev's Grisu-based C++ implementation. |
| emyg | C version of Milo Yip's Grisu2 implementation by Doug Currie. |
| grisu2 | Florian Loitsch's Grisu2 C implementation [1]. |
| to_chars | std::to_chars(); |
| stb | Jeff Roberts's & Sean Barrett's snprintf() implementation. |
| milo | Milo Yip's Grisu2 C++ implementation for RapidJSON. |
| fmt | Victor Zverovich's Grisu2 implementation for {fmt} C++ library. |
| fpconv | Andreas Samoljuk's Grisu2 C implementation. |
| doubleconv | C++ implementation extracted from Google's V8 JavaScript Engine with EcmaScriptConverter().ToShortest() (based on Grisu3, fall back to slower bignum algorithm when Grisu3 failed to produce shortest implementation). |
| format | C++20 stdlib, std::format("{:.17g}") |
| sprintf | sprintf() in C standard library with "%.17g" format. |
| ostrstream | traditional std::ostrstream in C++ standard library with setprecision(17). |
| ostringstream | traditional std::ostringstream in C++ standard library with setprecision(17). |
| dragon4 | A copy of Ryan Juckett's Dragon4 implementations, see http://www.ryanjuckett.com/programming/printing-floating-point-numbers/ with modifications for IEEE unbiased rounding and a bugfix. |
David M. Gay's dtoa() C implementation. Disabled because of invalid results and/or SIGSEGV. |
|
Marcandrysco's dtoa() C implementation. Disabled because of endless loop and/or invalid results and/or SIGSEGV. |
-
How to add an implementation?
You may clone an existing implementation file, then modify it and add to
CMakeLists.txt. Re-runcmaketo re-configure and re-build benchmark. Note that it will automatically register to the benchmark by macroREGISTER_TEST(name).Making pull request of new implementations is welcome.
-
Why not converting
doubletostd::string?It may introduce heap allocation, which is a big overhead. User can easily wrap these low-level functions to return
std::string, if needed. -
Why fast
dtoa()functions is needed?They are a very common operations in writing data in text format. The standard way of
sprintf(),std::stringstream, often provides poor performance.
[1] Loitsch, Florian. "Printing floating-point numbers quickly and accurately with integers." ACM Sigplan Notices 45.6 (2010): 233-243.
To be supplemented...
