1 /* 2 * A faster replacement for some subset of std.stdio 3 * Copyright © 2022, Siarhei Siamashka 4 * 5 * Boost Software License - Version 1.0 - August 17th, 2003 6 * 7 * Permission is hereby granted, free of charge, to any person or organization 8 * obtaining a copy of the software and accompanying documentation covered by 9 * this license (the "Software") to use, reproduce, display, distribute, 10 * execute, and transmit the Software, and to prepare derivative works of the 11 * Software, and to permit third-parties to whom the Software is furnished to 12 * do so, all subject to the following: 13 * 14 * The copyright notices in the Software and this entire statement, including 15 * the above license grant, this restriction and the following disclaimer, 16 * must be included in all copies of the Software, in whole or in part, and 17 * all derivative works of the Software, unless such copies or derivative 18 * works are solely in the form of machine-executable object code generated by 19 * a source language processor. 20 * 21 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 22 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 23 * FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 24 * SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 25 * FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 26 * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 27 * DEALINGS IN THE SOFTWARE. 28 */ 29 module speedy.stdio; 30 @safe: 31 32 private const BUFSIZE = 32 * 1024; 33 34 public import std.stdio; 35 import std.range, std.format, std.algorithm, std.traits, core.bitop; 36 import std.exception; 37 38 private enum SpeedySafety 39 { 40 Safe, 41 Unsafe 42 } 43 44 package template SpeedyWriter(SpeedySafety safety) 45 { 46 private static bool isChar(T)() 47 { 48 return isSomeChar!T && T.sizeof == 1; 49 } 50 51 private static bool isString(T)() 52 { 53 return is(T == string) || is(T == immutable(string)); 54 } 55 56 private static bool isNogcCompatibleNested(T)() 57 { 58 static if (isIntegral!T) 59 return true; 60 else static if (isInputRange!T || isArray!T) 61 return isNogcCompatibleNested!(ElementType!T); 62 else 63 return false; 64 } 65 66 private static bool isNogcCompatible(T)() 67 { 68 static if (isIntegral!T || isString!T || isChar!T) 69 return true; 70 else static if (isInputRange!T || isArray!T) 71 return isNogcCompatibleNested!(ElementType!T); 72 else 73 return false; 74 } 75 76 static if (safety == SpeedySafety.Safe) 77 { 78 static char[BUFSIZE] outbuf; 79 static ptrdiff_t outbuf_size = 0; 80 } 81 else 82 { 83 shared static char[BUFSIZE] outbuf; 84 shared static ptrdiff_t outbuf_size = 0; 85 } 86 87 version (unittest) 88 { 89 static void rawWriteStdout()(char[] buffer) 90 { 91 } 92 93 static bool buffer_contains()(string expected_data) @trusted 94 { 95 return expected_data == cast(string) outbuf[0 .. outbuf_size]; 96 } 97 } 98 else static if (safety == SpeedySafety.Safe) 99 { 100 version (Posix) 101 { 102 import core.sys.posix.stdio; 103 } 104 version (Windows) 105 { 106 alias flockfile = _lock_file; 107 alias funlockfile = _unlock_file; 108 } 109 shared static FILE* fp; 110 shared static this() @trusted 111 { 112 fp = std.stdio.stdout.getFP; 113 } 114 115 static bool flocked; 116 static void rawWriteStdout()(char[] buffer) @trusted 117 { 118 if (!flocked) 119 { 120 flocked = true; 121 flockfile(fp); 122 } 123 fwrite(cast(void*) buffer.ptr, 1, buffer.length, fp); 124 } 125 } 126 else version (Posix) 127 { 128 129 static void rawWriteStdout()(char[] buffer) @trusted 130 { 131 import core.sys.posix.unistd : write, STDOUT_FILENO; 132 import core.sys.posix.fcntl, core.stdc.errno; 133 134 while (buffer.length > 0) 135 { 136 auto result = write(STDOUT_FILENO, cast(void*) buffer.ptr, buffer.length); 137 if (result > 0) 138 { 139 buffer = buffer[result .. $]; 140 continue; 141 } 142 assert(result == -1 && (errno == EAGAIN || errno == EINTR), "unexpected error type"); 143 } 144 } 145 } 146 else version (Windows) 147 { 148 static void rawWriteStdout()(char[] buffer) @trusted 149 { 150 import core.sys.windows.windows; 151 152 auto h = GetStdHandle(STD_OUTPUT_HANDLE); 153 while (buffer.length > 0) 154 { 155 uint actual_written; 156 auto result = WriteFile(h, cast(void*) buffer.ptr, cast(uint) buffer.length, &actual_written, null); 157 if (result && actual_written > 0) 158 { 159 buffer = buffer[actual_written .. $]; 160 continue; 161 } 162 assert(result, "unexpected error"); 163 } 164 } 165 } 166 167 static void flush()() @trusted 168 { 169 if (outbuf_size > 0) 170 rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]); 171 outbuf_size = 0; 172 } 173 174 static void done()() @trusted 175 { 176 static if (safety == SpeedySafety.Safe) 177 { 178 if (flocked) 179 { 180 flush(); 181 funlockfile(fp); 182 flocked = false; 183 } 184 else 185 { 186 flocked = true; 187 flush(); 188 flocked = false; 189 } 190 } 191 } 192 193 shared static ~this() 194 { 195 flush(); 196 } 197 198 static void put(T)(T ch) @trusted if (isChar!T) 199 { 200 if (outbuf_size < outbuf.sizeof) 201 { 202 outbuf[outbuf_size] = ch; 203 outbuf_size = outbuf_size + 1; 204 } 205 else 206 { 207 rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]); 208 outbuf[0] = ch; 209 outbuf_size = 1; 210 } 211 } 212 213 static void ensure_outbuf_capacity()(ptrdiff_t size) @trusted 214 { 215 if (outbuf_size + size > outbuf.sizeof) 216 { 217 rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]); 218 outbuf_size = 0; 219 assert(size <= outbuf.sizeof, "write buffer is too small"); 220 } 221 } 222 223 // Compute the number of digits of an integer quickly. Public domain code from Daniel Lemire: 224 // * https://github.com/lemire/Code-used-on-Daniel-Lemire-s-blog 225 // * https://lemire.me/blog/2021/06/03/computing-the-number-of-digits-of-an-integer-even-faster/ 226 static int fast_digit_count()(uint x) 227 { 228 static immutable table = [ 229 4294967296, 8589934582, 8589934582, 8589934582, 12884901788, 230 12884901788, 12884901788, 17179868184, 17179868184, 17179868184, 231 21474826480, 21474826480, 21474826480, 21474826480, 25769703776, 232 25769703776, 25769703776, 30063771072, 30063771072, 30063771072, 233 34349738368, 34349738368, 34349738368, 34349738368, 38554705664, 234 38554705664, 38554705664, 41949672960, 41949672960, 41949672960, 235 42949672960, 42949672960 236 ]; 237 assert(x != 0); 238 return (x + table[bsr(x)]) >> 32; 239 } 240 241 static int fast_digit_count()(ulong x) 242 { 243 static immutable table = [ 244 4503599627370495, 9007199254740982, 9007199254740982, 245 9007199254740982, 13510798882111438, 13510798882111438, 246 13510798882111438, 18014398509481484, 18014398509481734, 247 18014398509481734, 22517998136849980, 22517998136849980, 248 22517998136851230, 22517998136851230, 27021597764210476, 249 27021597764210476, 27021597764216726, 31525197391530972, 250 31525197391530972, 31525197391530972, 36028797018651468, 251 36028797018651468, 36028797018651468, 36028797018651468, 252 40532396644771964, 40532396644771964, 40532396644771964, 253 45035996258079960, 45035996265892460, 45035996265892460, 254 49539595822950456, 49539595822950456, 49539595862012956, 255 49539595862012956, 54043195137820952, 54043195137820952, 256 54043195333133452, 58546793202691448, 58546793202691448, 257 58546793202691448, 63050385017561944, 63050385017561944, 258 63050385017561944, 63050385017561944, 67553945582432440, 259 67553945582432440, 67553945582432440, 72057105756677936, 260 72057349897302936, 72057349897302936, 76558752259048432, 261 76558752259048432, 76559972962173432, 76559972962173432, 262 81052586261418928, 81052586261418928, 81058689777043928, 263 85507357763789424, 85507357763789424, 85507357763789424, 264 89766816766159920, 89766816766159920, 89766816766159920, 265 89766816766159920 266 ]; 267 assert(x != 0); 268 int floor_log2_x = bsr(x); 269 return (table[floor_log2_x] + (x >> (floor_log2_x / 4))) >> 52; 270 } 271 272 // Construct digits conversion table at compile time 273 274 static immutable digitsbatch = 4; 275 276 static immutable tbl = { 277 char[(10 ^^ digitsbatch) * digitsbatch] result; 278 foreach (i; 0 .. (10 ^^ digitsbatch)) 279 { 280 int v = i; 281 foreach_reverse (j; 0 .. digitsbatch) 282 { 283 result[i * digitsbatch + j] = '0' + v % 10; 284 v /= 10; 285 } 286 } 287 return result; 288 }(); 289 290 static void put(T)(T val) if (isIntegral!T) 291 { 292 if (val == 0) 293 { 294 put('0'); 295 return; 296 } 297 if (val < 0) 298 { 299 put('-'); 300 val = -val; 301 } 302 Unsigned!T v = val; 303 304 ptrdiff_t ndigits = fast_digit_count(v); 305 ensure_outbuf_capacity(ndigits); 306 307 ptrdiff_t i = outbuf_size + ndigits - digitsbatch; 308 while (i >= outbuf_size) 309 { 310 auto newv = v / (10 ^^ digitsbatch); 311 auto rem = v - newv * (10 ^^ digitsbatch); 312 v = newv; 313 ptrdiff_t j = cast(ptrdiff_t) rem * digitsbatch; 314 outbuf[i .. i + digitsbatch] = tbl[j .. j + digitsbatch]; 315 i -= digitsbatch; 316 } 317 if (i < outbuf_size) 318 { 319 size_t lastbatch = digitsbatch - (outbuf_size - i); 320 ptrdiff_t j = cast(ptrdiff_t) v * digitsbatch + digitsbatch - lastbatch; 321 static if (digitsbatch <= 4) 322 { 323 i = outbuf_size; 324 if (lastbatch & 2) 325 { 326 outbuf[i .. i + 2] = tbl[j .. j + 2]; 327 i += 2; 328 j += 2; 329 } 330 if (lastbatch & 1) 331 outbuf[i] = tbl[j]; 332 } 333 else 334 { 335 outbuf[outbuf_size .. outbuf_size + lastbatch] = tbl[j .. j + lastbatch]; 336 } 337 } 338 outbuf_size = outbuf_size + ndigits; 339 } 340 341 static void put()(string str) @trusted 342 { 343 while (str.length > 0) 344 { 345 if (outbuf_size == outbuf.sizeof) 346 { 347 rawWriteStdout(cast(char[]) outbuf[0 .. outbuf_size]); 348 outbuf_size = 0; 349 } 350 ptrdiff_t chunksize = min(str.length, outbuf.sizeof - outbuf_size); 351 outbuf[outbuf_size .. outbuf_size + chunksize] = str[0 .. chunksize]; 352 str = str[chunksize .. $]; 353 outbuf_size = outbuf_size + chunksize; 354 } 355 } 356 357 static void put(Range)(Range r) if ((isInputRange!Range || isArray!Range) && isNogcCompatible!(ElementType!Range)) 358 { 359 static if (isChar!(ElementType!Range)) 360 { 361 put_with_joiner(r, ""); 362 } 363 else 364 { 365 put('['); 366 put_with_joiner(r, ", "); 367 put(']'); 368 } 369 } 370 371 static void put_with_joiner(Range, T)(Range r, T sep) 372 if ((isInputRange!Range || isArray!Range) && (isString!T || isChar!T)) 373 { 374 bool first = true; 375 foreach (x; r) 376 { 377 if (!first) 378 put(sep); 379 static if (isNogcCompatible!(typeof(x))) 380 put(x); 381 else 382 put_gc(x); 383 first = false; 384 } 385 } 386 387 // fallback to std.conv for the types, which are not supported 388 static void put_gc(T)(T val) 389 { 390 import std.conv; 391 392 put(val.to!string); 393 } 394 395 private struct SpeedyWriterSink 396 { 397 void put(T)(T arg) 398 { 399 SpeedyWriter!safety.put(arg); 400 } 401 } 402 403 static SpeedyWriterSink s; 404 405 static void writef_speedy(alias fmt, A...)(A args) 406 { 407 static immutable xfmt = parsefmt(fmt); 408 put(xfmt.prefix); 409 foreach (i, arg; args) 410 { 411 static if (xfmt.tokens[i].compound) 412 put_with_joiner(arg, xfmt.tokens[i].separator); 413 else 414 put(arg); 415 put(xfmt.tokens[i].suffix); 416 } 417 } 418 419 public static void writef(alias fmt, A...)(A args) 420 { 421 static if (isSpeedyCompatibleFormat!(fmt, A)) 422 writef_speedy!fmt(args); 423 else 424 formattedWrite!fmt(s, args); 425 done(); 426 } 427 428 public static void writef(Char, A...)(in Char[] fmt, A args) 429 { 430 formattedWrite(s, fmt, args); 431 done(); 432 } 433 434 public static void writefln(alias fmt, A...)(A args) 435 { 436 static if (isSpeedyCompatibleFormat!(fmt, A)) 437 writef_speedy!fmt(args); 438 else 439 formattedWrite!fmt(s, args); 440 put('\n'); 441 done(); 442 } 443 444 public static void writefln(Char, A...)(in Char[] fmt, A args) 445 { 446 formattedWrite(s, fmt, args); 447 put('\n'); 448 done(); 449 } 450 451 public static void write(T...)(T args) 452 { 453 foreach (arg; args) 454 static if (isNogcCompatible!(typeof(arg))) 455 put(arg); 456 else 457 put_gc(arg); 458 done(); 459 } 460 461 public static void writeln(T...)(T args) 462 { 463 foreach (arg; args) 464 static if (isNogcCompatible!(typeof(arg))) 465 put(arg); 466 else 467 put_gc(arg); 468 put('\n'); 469 done(); 470 } 471 472 // Similar to checkFormatException from Phobos, but only accepts a small 473 // subset of possible formats. 474 enum isSpeedyCompatibleFormat(alias fmt, Args...) = 475 { 476 try 477 { 478 void tassert(T)(T cond) 479 { 480 if (!cond) 481 throw new Exception(""); 482 } 483 484 static immutable xfmt = parsefmt(fmt); 485 tassert(xfmt.tokens.length == Args.length); 486 foreach (i, arg; Args.init) 487 { 488 if (xfmt.tokens[i].compound == 1 && (isInputRange!(typeof(arg)) || isArray!(typeof(arg)))) 489 { 490 if (isIntegral!(ElementType!(typeof(arg)))) 491 tassert(!find("ds", xfmt.tokens[i].spec).empty); 492 else if (isNogcCompatibleNested!(ElementType!(typeof(arg)))) 493 tassert(!find("s", xfmt.tokens[i].spec).empty); 494 else 495 tassert(false); 496 } 497 else if (xfmt.tokens[i].compound == 2 && (isInputRange!(typeof(arg)) || isArray!(typeof(arg)))) 498 { 499 if (isIntegral!(ElementType!(typeof(arg)))) 500 tassert(!find("ds", xfmt.tokens[i].spec).empty); 501 else if (isNogcCompatible!(ElementType!(typeof(arg)))) 502 tassert(!find("s", xfmt.tokens[i].spec).empty); 503 else 504 tassert(false); 505 } 506 else if (!xfmt.tokens[i].compound) 507 { 508 if (isIntegral!(typeof(arg))) 509 tassert(!find("ds", xfmt.tokens[i].spec).empty); 510 else if (isNogcCompatible!(typeof(arg))) 511 tassert(!find("s", xfmt.tokens[i].spec).empty); 512 else 513 tassert(false); 514 } 515 else 516 { 517 tassert(false); 518 } 519 520 } 521 } 522 catch (Exception e) 523 return false; 524 return true; 525 }(); 526 527 } 528 529 /////////////////////////////////////////////////////////////////////////////// 530 531 private struct FmtToken 532 { 533 char spec; 534 byte compound; /* 0 - not a compound, 1 - escaped, 2 - non-escaped */ 535 string separator; 536 string suffix; 537 } 538 539 private struct ParsedFmt 540 { 541 string prefix; 542 FmtToken[] tokens; 543 } 544 545 /* 546 * This is a minimalistic parser, which recognizes the following patterns: 547 * /%%/ - escaped '%' character 548 * /%d/ - decimal number 549 * /%s/ - string 550 * /%\(%[ds][^%]*%\)/ - escaped compound 551 * /%\-\(%[ds][^%]*%\)/ - non-escaped compound 552 */ 553 private ParsedFmt parsefmt(string fmt) 554 { 555 string scan_until_spec(ref string fmt) 556 { 557 string prefix = ""; 558 while (!fmt.empty) 559 { 560 auto tmp = fmt.find('%'); 561 if (tmp.empty) 562 { 563 prefix ~= fmt; 564 fmt = fmt[$ .. $]; 565 return prefix; 566 } 567 else 568 { 569 enforce(tmp.length >= 2); 570 if (tmp[1] == '%') 571 { 572 prefix ~= fmt[0 .. fmt.length - tmp.length + 1]; 573 fmt = fmt[fmt.length - tmp.length + 2 .. $]; 574 } 575 else 576 { 577 prefix ~= fmt[0 .. fmt.length - tmp.length]; 578 fmt = fmt[fmt.length - tmp.length .. $]; 579 return prefix; 580 } 581 } 582 } 583 return prefix; 584 } 585 586 ParsedFmt result; 587 result.prefix = scan_until_spec(fmt); 588 while (!fmt.empty) 589 { 590 enforce(fmt.length >= 2 && fmt[0] == '%'); 591 if (fmt[1] == '(' || (fmt.length >= 3 && fmt[1] == '-' && fmt[2] == '(')) 592 { 593 byte compound = 1; 594 if (fmt[1] == '-') 595 { 596 compound = 2; 597 fmt = fmt[1 .. $]; 598 } 599 enforce(fmt.length >= 6 && fmt[2] == '%'); // at least %(%s%) 600 char spec = fmt[3]; 601 fmt = fmt[4 .. $]; 602 string separator = scan_until_spec(fmt); 603 enforce(fmt.length >= 2 && fmt[1] == ')'); 604 fmt = fmt[2 .. $]; 605 string suffix = scan_until_spec(fmt); 606 result.tokens ~= FmtToken(spec, compound, separator, suffix); 607 } 608 else 609 { 610 byte spec = fmt[1]; 611 fmt = fmt[2 .. $]; 612 string suffix = scan_until_spec(fmt); 613 result.tokens ~= FmtToken(spec, 0, "", suffix); 614 } 615 } 616 return result; 617 } 618 619 alias write = SpeedyWriter!(SpeedySafety.Safe).write; 620 alias writeln = SpeedyWriter!(SpeedySafety.Safe).writeln; 621 alias writef = SpeedyWriter!(SpeedySafety.Safe).writef; 622 alias writefln = SpeedyWriter!(SpeedySafety.Safe).writefln; 623 624 alias unsafe_write = SpeedyWriter!(SpeedySafety.Unsafe).write; 625 alias unsafe_writeln = SpeedyWriter!(SpeedySafety.Unsafe).writeln; 626 alias unsafe_writef = SpeedyWriter!(SpeedySafety.Unsafe).writef; 627 alias unsafe_writefln = SpeedyWriter!(SpeedySafety.Unsafe).writefln; 628 alias unsafe_stdout_flush = SpeedyWriter!(SpeedySafety.Unsafe).flush; 629 630 version (unittest) 631 { 632 alias unsafe_stdout_buffer_contains = SpeedyWriter!(SpeedySafety.Unsafe).buffer_contains; 633 }