Input and Output Optimization

This article introduces how to optimize stream-based I/O and C-style I/O.

Note

The actual speeds of stream-based I/O and C-style I/O vary to some extent with the environment (such as the compiler, operating system, and hardware specifications). If you want to perform further analysis, use experimental results as the basis. However, pay attention to controlling variables in the experiments to avoid incorrect conclusions caused by multiple variables.

Stream-Based I/O

For stream-based I/O (such as std::cin and std::cout), the most common optimization methods are disabling synchronization with C streams and untieing input and output streams.

Disabling Synchronization

Use the std::ios::sync_with_stdio(false) function to disable synchronization with C streams. For compatibility with C—that is, to ensure that no confusion occurs when printf and std::cout are used at the same time—C++ synchronizes these two types of streams. Synchronized C++ streams are guaranteed to be thread-safe.

This is actually a conservative measure adopted by C++ for compatibility. When synchronization is enabled, at every I/O operation, the C++ stream immediately applies the operation to the corresponding C buffer. If the code does not involve C-style I/O, this operation is redundant. Therefore, synchronization with C streams can be disabled before performing I/O operations, but note that after doing so, std::cin and scanf cannot be used at the same time in subsequent code, nor can std::cout and printf; however, std::cin and printf can be used together, as can scanf and std::cout.

Untieing Streams

Use the tie() function to untie input and output streams.

By default, std::cin is tied to &std::cout, so std::cout.flush() must be called every time formatted input is performed to clear the output buffer, which increases the I/O overhead. You can untie them with std::cin.tie(nullptr) to further improve execution efficiency.

Note

When using it, do not omit the argument and write std::cin.tie(). This does not untie the streams, but returns the output stream tied to std::cin. It is also unnecessary to call std::cout.tie(nullptr), because by default no other output stream is tied to std::cout.

Implementation

std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
Note

After performing both operations above, the program must manually flush to ensure that the content displayed by each std::cout appears before std::cin. This is because, in this case, when std::cin is called, std::cout does not automatically flush its buffer. For example:

std::ios::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout << "Please input your name: "
          << std::flush;  // or: std::endl;
                          // Because each call to std::endl flushes the output buffer, while \n
                          // does not.
// If std::flush is removed, the prompt will not be displayed before the name is input.
std::cin >> name;

C-Style I/O

There is still room to improve the efficiency of scanf and printf; the improvements are all based on conversion between integers and strings.

Note

The input and output optimizations introduced on this page target integer data. Optimizing floating-point input and output is very complicated. For input-related optimization, see the Bellerophon algorithm; for output-related optimization, see the Ryū algorithm.

Design

Note

The current optimization methods focus on faster I/O, while using naive methods during data conversion and not fully exploiting hardware features. Nowadays, the vast majority of CPUs with the x86 architecture support the AVX2 instruction set, which can be used to accelerate conversion between integers and strings with SIMD. Standard library functions do not use SIMD optimization; for example, the implementation in libstdc++ converts two consecutive digits at a time through a lookup table. Therefore, optimizing the data-conversion process may also yield gains. However, in the context of contests, the optimization methods mentioned in this article are sufficient for the vast majority of scenarios.

Input Optimization

Each integer consists of a sign and a digit part, and the sign always comes before the digit part, so the sign part is read first. For the sign part, the + sign of a positive integer is usually omitted and does not affect the value represented by the following digits, whereas - cannot be omitted and therefore needs to be checked. If the input contains no negative integers, this check can be omitted. The digit part contains only digits from 0 to 9, so when a character that cannot occur in an integer (usually a space) is read, it can be determined that this integer has finished being read.

When reading, because the digits are read from left to right, the Qin Jiushao algorithm can be used for integer conversion. Thus, the entire conversion process can be combined with input.

During the process of reading the digit part, it is necessary to determine whether the character read is a decimal digit character. This can be checked simply with the condition ch >= '0' && ch <= '9', or with the isdigit() function.

Output Optimization

When outputting, an integer must be converted into a string. The usual approach is the naive algorithm: directly calculate each digit of the integer from low to high, convert it to a character, and output the result in reverse order.

Implementation Details

Integer Overflow

Integer overflow must be considered in the implementation. For example, improperly taking the opposite number in output optimization can cause the minimum integer value to exceed the maximum value representable by that integer type after being negated, which may lead to incorrect output. Similar overflow may occur when reading the minimum integer value, but in this case it may not cause an error in the input data, because the value obtained through overflow may equal the actual input value.

Signed integer overflow is undefined behavior. In an implementation, the above problems can be avoided by using the property of integer division of negative numbers in C that it rounds toward zero. However, this issue does not arise if negative numbers do not need to be input or output, or if the minimum value of this integer type cannot be input or output.

Improving Generality

If a program uses integer variables of multiple types, it may be necessary to implement multiple input and output functions with different types but the same logic. In this case, C++ template can be used to implement input and output optimization for all integer types. For example, under the C++11 standard, use

template <typename T>
typename std::enable_if<std::is_integral<T>::value &&
                        std::is_signed<T>::value>::type
read(T &x);

or under the C++20 standard, use

template <std::signed_integral T>
void read(T &x);

to define the function.

For readability, the implementations later in this article assume that only integers of type int need to be read. These implementations are sufficient for most problems.

Implementation

Mainstream implementations differ only in the input and output functions they use; their logic for integer conversion is the same. The following sections introduce them according to the input and output functions used by each implementation.

Using getchar and putchar

The core code is as follows.

void read(int &x) {
  bool neg = false;
  x = 0;
  char ch = 0;
  while (ch < '0' || ch > '9') {
    if (ch == '-') neg = true;
    ch = getchar();
  }
  if (neg) {
    while (ch >= '0' && ch <= '9') {
      x = x * 10 + ('0' - ch);
      ch = getchar();
    }
  } else {
    while (ch >= '0' && ch <= '9') {
      x = x * 10 + (ch - '0');
      ch = getchar();
    }
  }
}

void write(int x) {
  bool neg = false;
  if (x < 0) {
    neg = true;
    putchar('-');
  }
  static int sta[40];
  int top = 0;
  do {
    sta[top++] = x % 10;
    x /= 10;
  } while (x);
  if (neg)
    while (top) putchar('0' - sta[--top]);
  else
    while (top) putchar('0' + sta[--top]);
}

Using fread and fwrite

Faster input and output can be implemented with fread and fwrite. Their function signatures are as follows.

std::size_t fread(void* buffer, std::size_t size, std::size_t count,
                  std::FILE* stream);
std::size_t fwrite(const void* buffer, std::size_t size, std::size_t count,
                   std::FILE* stream);

For example, fread(Buf, 1, SIZE, stdin) means reading SIZE data blocks of size 1 byte from standard input into Buf. The return value indicates how many bytes of data were successfully read.

Because fread and fwrite read and write an entire block at a time, they are faster than getchar() and putchar(). If the buffer is large enough, the entire file can be read at once. If the buffer is not large enough, however, multiple reads are needed to ensure that all input is read. To implement this functionality, only getchar needs to be redefined.

char buf[1 << 20], *p1, *p2;
#define gc()                                                               \
  (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, 1 << 20, stdin), p1 == p2) \
       ? EOF                                                               \
       : *p1++)

Output is similar to input: first place the output in a buffer, and finally output the buffer's contents at once with fwrite.

The core code is as follows.

// #define DEBUG 1  // Debug switch
struct IO {
#define MAXSIZE (1 << 20)
#define isdigit(x) (x >= '0' && x <= '9')
  char buf[MAXSIZE], *p1, *p2;
  char pbuf[MAXSIZE], *pp;
#if DEBUG
#else
  IO() : p1(buf), p2(buf), pp(pbuf) {}

  ~IO() { fwrite(pbuf, 1, pp - pbuf, stdout); }
#endif
  char gc() {
#if DEBUG  // Debugging; characters can be displayed
    return getchar();
#endif
    if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, MAXSIZE, stdin);
    return p1 == p2 ? ' ' : *p1++;
  }

  void read(int &x) {
    bool neg = false;
    x = 0;
    char ch = gc();
    for (; !isdigit(ch); ch = gc())
      if (ch == '-') neg = true;
    if (neg)
      for (; isdigit(ch); ch = gc()) x = x * 10 + ('0' - ch);
    else
      for (; isdigit(ch); ch = gc()) x = x * 10 + (ch - '0');
  }

  void read(char *s) {
    char ch = gc();
    for (; isspace(ch); ch = gc());
    for (; !isspace(ch); ch = gc()) *s++ = ch;
    *s = 0;
  }

  void read(char &c) { for (c = gc(); isspace(c); c = gc()); }

  void push(const char &c) {
#if DEBUG  // Debugging; characters can be displayed
    putchar(c);
#else
    if (pp - pbuf == MAXSIZE) fwrite(pbuf, 1, MAXSIZE, stdout), pp = pbuf;
    *pp++ = c;
#endif
  }

  void write(int x) {
    bool neg = false;
    if (x < 0) {
      neg = true;
      push('-');
    }
    static int sta[40];
    int top = 0;
    do {
      sta[top++] = x % 10;
      x /= 10;
    } while (x);
    if (neg)
      while (top) push('0' - sta[--top]);
    else
      while (top) push('0' + sta[--top]);
  }

  void write(int x, char lastChar) { write(x), push(lastChar); }
} io;

When using this method, note the following:

  • When the debug switch is disabled, fread() and fwrite() are used, and fwrite() is automatically executed during destruction on exit. When the debug switch is enabled, getchar() and putchar() are used to facilitate debugging.
  • To perform file I/O, add freopen() before all reading and writing operations.

Using mmap

mmap is a Linux system call that can map an entire file into memory, similar to a memory region that can be referenced by a pointer, and can be faster in some scenarios. Its function signature is as follows:

void *mmap(void addr[.length], size_t length, int prot, int flags, int fd,
           off_t offset);
Note

mmap cannot be used in Windows environments (for example, the judging systems of CodeForces and HDU), and its use in official contests is not recommended either. In fact, fread is already fast enough; moreover, if mmap is used to repeatedly read a small part of a file, the overhead of performing a memory mapping and having the kernel handle page faults is much greater than that of using fread.

First obtain the file descriptor fd, then obtain the file size through fstat, and finally use mmap to obtain the pointer *pc to the file mapped in memory. After that, *pc++ can directly replace getchar() for reading the file.

When reading from standard input, fd can be set to 0. However, using mmap on standard input is extremely dangerous, and input cannot be entered in a terminal; you can redirect a file to standard input instead.

Example problem: Luogu P10815 [Template] Fast Input

Read nn integers in the range [−n,n][-n, n], sum them, and output the result. Here n≤108n \leq 10^8. The data guarantees that for every prefix of the sequence, the sum of that prefix lies within the storage range of a 3232-bit signed integer.

The reference code is as follows.

#include <fcntl.h>
#include <sys/mman.h>
#include <sys/stat.h>
#include <unistd.h>

#include <cstdio>

char *pc;

void rd(int &x) {
  bool neg = false;
  x = 0;
  char ch = 0;
  while (ch < '0' || ch > '9') {
    if (ch == '-') neg = true;
    ch = *pc++;
  }
  if (neg) {
    while (ch >= '0' && ch <= '9') {
      x = x * 10 + ('0' - ch);
      ch = *pc++;
    }
  } else {
    while (ch >= '0' && ch <= '9') {
      x = x * 10 + (ch - '0');
      ch = *pc++;
    }
  }
}

int main() {
  int fd = 0;  // Read from stdin
  // int fd = open("xxx.in", O_RDONLY); // Read from a file
  struct stat state;
  fstat(fd, &state);  // Obtain the file size
  pc = (char *)mmap(NULL, state.st_size, PROT_READ, MAP_PRIVATE, fd, 0);
  int n, x, sum = 0;
  rd(n);
  while (n--) {
    rd(x);
    sum += x;
  }
  printf("%d\n", sum);
}

References

Accelerating input and output with cin.tie and sync_with_stdio - Coder's Field

C++ optimization - Heavy Watal

'Re: mmap/mlock performance versus read' - MARC

This content comes from OI Wiki: Input and Output Optimization. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.