├── .gitignore ├── LICENSE ├── Makefile ├── README.md └── parse_integers_fast.cc /.gitignore: -------------------------------------------------------------------------------- 1 | release/ 2 | debug/ 3 | random_ints_1600000.txt 4 | *~ 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Maxim Egorushkin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | BUILD := release 3 | 4 | TOOLSET := gcc 5 | build_dir := ${CURDIR}/${BUILD}/${TOOLSET} 6 | 7 | CXX.gcc := g++ 8 | CC.gcc := gcc 9 | LD.gcc := g++ 10 | AR.gcc := gcc-ar 11 | 12 | CXX.gcc-8 := g++-8 13 | CC.gcc-8 := gcc-8 14 | LD.gcc-8 := g++-8 15 | AR.gcc-8 := gcc-ar-8 16 | 17 | CXX.clang := clang++ 18 | CC.clang := clang 19 | LD.clang := clang++ 20 | AR.clang := ar 21 | 22 | CXX := ${CXX.${TOOLSET}} 23 | CC := ${CC.${TOOLSET}} 24 | LD := ${LD.${TOOLSET}} 25 | AR := ${AR.${TOOLSET}} 26 | 27 | CXXFLAGS.gcc.debug := -Og -fstack-protector-all -fno-omit-frame-pointer # -D_GLIBCXX_DEBUG 28 | CXXFLAGS.gcc.release := -O3 -march=native -ffast-math -DNDEBUG 29 | CXXFLAGS.gcc := -pthread -std=gnu++17 -march=native -W{all,extra,error} -g -fmessage-length=0 ${CXXFLAGS.gcc.${BUILD}} 30 | CXXFLAGS.gcc-8 := ${CXXFLAGS.gcc} 31 | 32 | CFLAGS.gcc := -pthread -march=native -W{all,extra} -g -fmessage-length=0 ${CXXFLAGS.gcc.${BUILD}} 33 | CFLAGS.gcc-8 := ${CFLAGS.gcc} 34 | 35 | CXXFLAGS.clang.debug := -O0 -fstack-protector-all 36 | CXXFLAGS.clang.release := -O3 -march=native -ffast-math -DNDEBUG 37 | CXXFLAGS.clang := -pthread -std=gnu++17 -march=native -W{all,extra,error} -g -fmessage-length=0 ${CXXFLAGS.clang.${BUILD}} 38 | 39 | CXXFLAGS := ${CXXFLAGS.${TOOLSET}} 40 | CFLAGS := ${CFLAGS.${TOOLSET}} 41 | 42 | CPPFLAGS := 43 | 44 | #LDFLAGS.gcc := -L${gcc_dir}/lib64 -Wl,-rpath=${gcc_dir}/lib64 45 | LDFLAGS.debug := 46 | LDFLAGS.release := 47 | LDFLAGS := -fuse-ld=gold -pthread -g ${LDFLAGS.${BUILD}} ${LDFLAGS.${TOOLSET}} 48 | LDLIBS := -lrt 49 | 50 | COMPILE.CXX = ${CXX} -c -o $@ ${CPPFLAGS} -MD -MP ${CXXFLAGS} $(abspath $<) 51 | COMPILE.S = ${CXX} -S -masm=intel -o- ${CPPFLAGS} ${CXXFLAGS} $(abspath $<) | c++filt > $@ 52 | PREPROCESS.CXX = ${CXX} -E -o $@ ${CPPFLAGS} ${CXXFLAGS} $(abspath $<) 53 | COMPILE.C = ${CC} -c -o $@ ${CPPFLAGS} -MD -MP ${CFLAGS} $(abspath $<) 54 | LINK.EXE = ${LD} -o $@ $(LDFLAGS) $(filter-out Makefile,$^) $(LDLIBS) 55 | LINK.SO = ${LD} -shared -o $@ $(LDFLAGS) $(filter-out Makefile,$^) $(LDLIBS) 56 | LINK.A = ${AR} rsc $@ $(filter-out Makefile,$^) 57 | 58 | all : ${build_dir}/parse_integers_fast 59 | 60 | ${build_dir} : 61 | mkdir -p $@ 62 | 63 | ${build_dir}/parse_integers_fast : ${build_dir}/parse_integers_fast.o Makefile | ${build_dir} 64 | $(strip ${LINK.EXE}) 65 | -include ${build_dir}/parse_integers_fast.d 66 | 67 | ${build_dir}/%.so : CXXFLAGS += -fPIC 68 | ${build_dir}/%.so : Makefile | ${build_dir} 69 | $(strip ${LINK.SO}) 70 | 71 | ${build_dir}/%.a : Makefile | ${build_dir} 72 | $(strip ${LINK.A}) 73 | 74 | random_ints_1600000.txt : 75 | echo 1600000 0 >$@ 76 | od --address-radix=n --read-bytes=$$((1600000*4)) --format=d4 /dev/urandom >>$@ 77 | 78 | run_parse_integers_fast : ${build_dir}/parse_integers_fast | random_ints_1600000.txt 79 | @echo "---- running $< ----" 80 | $< 81 | 82 | ${build_dir}/%.o : %.cc Makefile | ${build_dir} 83 | $(strip ${COMPILE.CXX}) 84 | 85 | ${build_dir}/%.o : %.c Makefile | ${build_dir} 86 | $(strip ${COMPILE.C}) 87 | 88 | %.S : %.cc Makefile | ${build_dir} 89 | $(strip ${COMPILE.S}) 90 | 91 | %.I : %.cc 92 | $(strip ${PREPROCESS.CXX}) 93 | 94 | clean : 95 | rm -rf ${build_dir} 96 | 97 | .PHONY : clean all run_parse_integers_fast run_% 98 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # parse-integers-benchmark 2 | Benchmarking reading and parsing integers from a file in C++ using one thread only. 3 | 4 | The methods benchmarked are: 5 | 6 | * `getchar-inline` - call `std::getchar` and parse each `int` char-by-char. 7 | * `scanf` - call `std::scanf` to parse each `int`. 8 | * `scanf-multi` - call `std::scanf` to parse 64 `int`s in one call. 9 | * `iostream` - call `std::istream::operator>>(int&)` to parse each `int`. No `multi` version is possible. 10 | * `mmap-inline` - `mmap` the entire file into memory and parse each `int` char-by-char. 11 | * `mmap-charconv` - `mmap` the entire file into memory and parse each `int` using `std::from_chars`. 12 | 13 | ## Building and running 14 | ``` 15 | git clone https://github.com/max0x7ba/parse-integers-benchmark.git 16 | cd parse-integers-benchmark 17 | make -r run_parse_integers_fast 18 | ``` 19 | 20 | The benchmark compiles in C++17 mode by default. You can search-replace `gnu++17` to `gnu++11` in `Makefile` to make it work with C++11. 21 | 22 | ## Example outputs 23 | ##### Ubuntu 18.04.4 LTS, g++-8.3.0, Intel Core i9-9900KS CPU @ 5.1GHz, C++17 `charconv`. 24 | ``` 25 | ---- Best times ---- 26 | seconds, percent, method 27 | 0.123534698, 100.0, scanf 28 | 0.121618955, 98.4, iostream 29 | 0.104887812, 84.9, scanf-multi 30 | 0.047167165, 38.2, getchar-inline 31 | 0.030817239, 24.9, mmap-charconv 32 | 0.025470340, 20.6, mmap-inline 33 | ``` 34 | ##### Ubuntu 18.04.2 LTS, g++-8.2.0, Intel Core i7-7700K CPU @ 5GHz 35 | ``` 36 | ---- Best times ---- 37 | seconds, percent, method 38 | 0.133155952, 100.0, iostream 39 | 0.102128208, 76.7, scanf 40 | 0.082469185, 61.9, scanf-multi 41 | 0.048661004, 36.5, getchar-inline 42 | 0.025320109, 19.0, mmap-inline 43 | ``` 44 | ##### CentOS release 6.10, g++-6.3.0, Intel Core i7-4790 CPU @ 3.6GHz 45 | ``` 46 | ---- Best times ---- 47 | seconds, percent, method 48 | 0.167985515, 100.0, getchar-inline 49 | 0.147258495, 87.7, scanf 50 | 0.137161991, 81.7, iostream 51 | 0.118859546, 70.8, scanf-multi 52 | 0.034033769, 20.3, mmap-inline 53 | ``` 54 | 55 | ## Notes 56 | * `mmap` is a relatively expensive operation so that it may only be faster for relatively large files. Benchmark on your file size. 57 | * `getchar-inline` and `mmap-inline` use a similar inline method of parsing an `int` from a stream of `char`, so that the time difference can be attributed to the latency of accessing the next `char`. `mmap` method maps the entire file into memory and reads it sequentially, which is the best case scenario for the CPU memory prefetcher. 58 | -------------------------------------------------------------------------------- /parse_integers_fast.cc: -------------------------------------------------------------------------------- 1 | /* -*- mode: c++; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 4 -*- */ 2 | #include 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | 9 | #if __cplusplus >= 201703L 10 | #include 11 | #endif 12 | 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | 19 | // https://stackoverflow.com/a/43870188/412080 20 | #define LIKELY(condition) __builtin_expect(static_cast(condition), 1) 21 | #define UNLIKELY(condition) __builtin_expect(static_cast(condition), 0) 22 | 23 | char const random_ints_filename[] = "random_ints_1600000.txt"; 24 | constexpr int ELEMENTS = 1600000; 25 | 26 | void method_istream(int(&a)[ELEMENTS]) { 27 | std::ios::sync_with_stdio(false); 28 | int n, m; 29 | std::cin >> n >> m; 30 | for(int i = 0; i < n; ++i) 31 | std::cin >> a[i]; 32 | } 33 | 34 | void method_scanf(int(&a)[ELEMENTS]) { 35 | int n, m; 36 | if(UNLIKELY(2 != std::scanf("%d %d", &n, &m))) 37 | throw; 38 | for(int i = 0; i < n; ++i) 39 | if(UNLIKELY(1 != std::scanf("%d", a + i))) 40 | throw; 41 | } 42 | 43 | constexpr int step = 64; 44 | char const fmt[step * 3] = 45 | "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d " 46 | "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d " 47 | "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d " 48 | "%d %d %d %d %d %d %d %d %d %d %d %d %d %d %d %d" 49 | ; 50 | 51 | void method_scanf_multi(int(&a)[ELEMENTS]) { 52 | int n, m; 53 | if(UNLIKELY(2 != std::scanf("%d %d", &n, &m))) 54 | throw; 55 | 56 | for(int i = 0; i < n; i += step) { 57 | int expected = step < n - i ? step : n - i; 58 | int* b = a + i; 59 | int read = scanf(fmt + 3 * (step - expected), 60 | b + 0x00, b + 0x01, b + 0x02, b + 0x03, b + 0x04, b + 0x05, b + 0x06, b + 0x07, 61 | b + 0x08, b + 0x09, b + 0x0a, b + 0x0b, b + 0x0c, b + 0x0d, b + 0x0e, b + 0x0f, 62 | b + 0x10, b + 0x11, b + 0x12, b + 0x13, b + 0x14, b + 0x15, b + 0x16, b + 0x17, 63 | b + 0x18, b + 0x19, b + 0x1a, b + 0x1b, b + 0x1c, b + 0x1d, b + 0x1e, b + 0x1f, 64 | b + 0x20, b + 0x21, b + 0x22, b + 0x23, b + 0x24, b + 0x25, b + 0x26, b + 0x27, 65 | b + 0x28, b + 0x29, b + 0x2a, b + 0x2b, b + 0x2c, b + 0x2d, b + 0x2e, b + 0x2f, 66 | b + 0x30, b + 0x31, b + 0x32, b + 0x33, b + 0x34, b + 0x35, b + 0x36, b + 0x37, 67 | b + 0x38, b + 0x39, b + 0x3a, b + 0x3b, b + 0x3c, b + 0x3d, b + 0x3e, b + 0x3f); 68 | if(UNLIKELY(read != expected)) 69 | throw; 70 | } 71 | } 72 | 73 | void method_getchar_inline(int(&a)[ELEMENTS]) { 74 | int n, m; 75 | if(UNLIKELY(2 != std::scanf("%d %d", &n, &m))) 76 | throw; 77 | 78 | for(int i = 0; i < n; ++i) { 79 | int r = std::getchar(); 80 | while(std::isspace(r)) 81 | r = std::getchar(); 82 | bool neg = false; 83 | if('-' == r) { 84 | neg = true; 85 | r = std::getchar(); 86 | } 87 | r -= '0'; 88 | for(;;) { 89 | int s = std::getchar(); 90 | if(!std::isdigit(s)) 91 | break; 92 | r = r * 10 + (s - '0'); 93 | } 94 | a[i] = neg ? -r : r; 95 | } 96 | } 97 | 98 | inline int int_parse_inline(char const*& begin, char const* end) { 99 | char const* p = begin; 100 | while(p != end && std::isspace(*p)) 101 | ++p; 102 | if(UNLIKELY(p == end)) 103 | throw; 104 | bool neg = *p == '-'; 105 | p += neg; 106 | int r = 0; 107 | do { 108 | unsigned c = *p - '0'; 109 | if(c >= 10) 110 | break; 111 | r = r * 10 + static_cast(c); 112 | } while(++p != end); 113 | begin = p; 114 | return neg ? -r : r; 115 | } 116 | 117 | template 118 | inline void method_mmap_(int(&a)[ELEMENTS]) { 119 | int fd = open(random_ints_filename, O_RDONLY); 120 | if(UNLIKELY(fd == -1)) 121 | throw; 122 | struct stat s; 123 | if(UNLIKELY(fstat(fd, &s))) 124 | throw; 125 | void* pfd = mmap(nullptr, s.st_size, PROT_READ, MAP_PRIVATE | MAP_POPULATE, fd, 0); 126 | if(UNLIKELY(pfd == MAP_FAILED)) 127 | throw; 128 | 129 | char const* begin = static_cast(pfd); 130 | char const* end = begin + s.st_size; 131 | 132 | int n = parse_int(begin, end); 133 | int m = parse_int(begin, end); 134 | static_cast(m); 135 | 136 | for(int i = 0; i < n; ++i) 137 | a[i] = parse_int(begin, end); 138 | } 139 | 140 | void method_mmap_inline(int(&a)[ELEMENTS]) { 141 | method_mmap_(a); 142 | } 143 | 144 | #if __cplusplus >= 201703L 145 | inline int int_parse_charconv(char const*& begin, char const* end) { 146 | int value; 147 | while(begin != end && std::isspace(*begin)) 148 | ++begin; 149 | auto r = std::from_chars(begin, end, value); 150 | if(UNLIKELY(static_cast(r.ec))) 151 | throw; 152 | begin = r.ptr; 153 | return value; 154 | } 155 | 156 | void method_mmap_charconv(int(&a)[ELEMENTS]) { 157 | method_mmap_(a); 158 | } 159 | #endif 160 | 161 | unsigned reverse_crc32(int const* begin, int const* end) { 162 | unsigned r = 0; 163 | while(begin != end) 164 | r = __builtin_ia32_crc32si(r, *--end); 165 | return r; 166 | } 167 | 168 | decltype(&method_istream) const methods[] = { 169 | &method_istream 170 | , &method_scanf 171 | , &method_scanf_multi 172 | , &method_getchar_inline 173 | , &method_mmap_inline 174 | #if __cplusplus >= 201703L 175 | , &method_mmap_charconv 176 | #endif 177 | }; 178 | char const* names[] = { 179 | "iostream" 180 | , "scanf" 181 | , "scanf-multi" 182 | , "getchar-inline" 183 | , "mmap-inline" 184 | #if __cplusplus >= 201703L 185 | , "mmap-charconv" 186 | #endif 187 | }; 188 | constexpr int method_count = sizeof methods / sizeof *methods; 189 | 190 | int main() { 191 | constexpr int RUNS = 10; 192 | 193 | printf("method,checksum,seconds\n"); 194 | double best_times[method_count]; 195 | for(int method = 0; method < method_count; ++method) { 196 | for(int run = 0; run < RUNS; ++run) { 197 | std::ifstream random_ints(random_ints_filename); 198 | auto old_buf = std::cin.rdbuf(random_ints.rdbuf()); 199 | if(UNLIKELY(!freopen(random_ints_filename, "rb", stdin))) 200 | throw; 201 | 202 | auto t0 = std::chrono::high_resolution_clock::now(); 203 | int a[ELEMENTS]; 204 | methods[method](a); 205 | auto t1 = std::chrono::high_resolution_clock::now(); 206 | 207 | // Make sure the array and the computations are not optimized away. 208 | unsigned sum = reverse_crc32(a, a + sizeof a / sizeof *a); 209 | 210 | double time = std::chrono::duration_cast>(t1 - t0).count(); 211 | std::printf("%s,%u,%.9f\n", names[method], sum, time); 212 | if(!run || time < best_times[method]) 213 | best_times[method] = time; 214 | 215 | std::cin.rdbuf(old_buf); 216 | } 217 | } 218 | 219 | std::printf("\n---- Best times ----\n"); 220 | std::printf("seconds, percent, method\n"); 221 | int indexes[method_count]; 222 | for(int i = 0; i < method_count; ++i) 223 | indexes[i] = i; 224 | std::sort(indexes, indexes + method_count, [&best_times](int a, int b) { return best_times[a] > best_times[b]; }); 225 | int worst = indexes[0]; 226 | for(int method = 0; method < method_count; ++method) { 227 | int i = indexes[method]; 228 | std::printf("%.9f, %6.1f, %s\n", best_times[i], 1e2 * best_times[i] / best_times[worst], names[i]); 229 | } 230 | } 231 | --------------------------------------------------------------------------------