讀入、輸出優化

RioTian發表於2020-05-25

在預設情況下, std::cin/std::cout 是極為遲緩的讀入/輸出方式,而 scanf/printfstd::cin/std::cout 快得多。

可是為什麼會這樣呢?有沒有什麼辦法解決讀入輸出緩慢的問題呢?

關閉同步/解除繫結

std::ios::sync_with_stdio(false)

這個函式是一個“是否相容 stdio”的開關,C++ 為了相容 C,保證程式在使用了 printfstd::cout 的時候不發生混亂,將輸出流綁到了一起。

這其實是 C++ 為了相容而採取的保守措施。我們可以在進行 IO 操作之前將 stdio 解除繫結,但是在這樣做之後要注意不能同時使用 std::cin/std::coutscanf/printf

tie

tie 是將兩個 stream 繫結的函式,空引數的話返回當前的輸出流指標。

在預設的情況下 std::cin 繫結的是 std::cout ,每次執行 << 操作符的時候都要呼叫 flush() ,這樣會增加 IO 負擔。可以通過 std::cin.tie(0) (0 表示 NULL)來解除 std::cinstd::cout 的繫結,進一步加快執行效率。

程式碼實現

std::ios::sync_with_stdio(false);
std::cin.tie(0);
// 如果編譯開啟了 C++11 或更高版本,建議使用 std::cin.tie(nullptr);

讀入優化

scanfprintf 依然有優化的空間,這就是本章所介紹的內容——讀入和輸出優化。

  • 注意,本頁面中介紹的讀入和輸出優化均針對整型資料,若要支援其他型別的資料(如浮點數),可自行按照本頁面介紹的優化原理來編寫程式碼。

原理

眾所周知, getchar 是用來讀入 1 byte 的資料並將其轉換為 char 型別的函式,且速度很快,故可以用“讀入字元——轉換為整型”來代替緩慢的讀入

每個整數由兩部分組成——符號和數字

整數的 '+' 通常是省略的,且不會對後面數字所代表的值產生影響,而 '-' 不可省略,因此要進行判定

10 進位制整數中是不含空格或除 0~9 和正負號外的其他字元的,因此在讀入不應存在於整數中的字元(通常為空格)時,就可以判定已經讀入結束

C 和 C++ 語言分別在 ctype.h 和 cctype 標頭檔案中,提供了函式 isdigit , 這個函式會檢查傳入的引數是否為十進位制數字字元,是則返回 true ,否則返回 false 。對應的,在下面的程式碼中,可以使用 isdigit(ch) 代替 ch >= '0' && ch <= '9' ,而可以使用 !isdigit(ch) 代替 ch <'0' || ch> '9'

程式碼實現

int read() {
  int x = 0, w = 1;
  char ch = 0;
  while (ch < '0' || ch > '9') {  // ch 不是數字時
    if (ch == '-') w = -1;        // 判斷是否為負
    ch = getchar();               // 繼續讀入
  }
  while (ch >= '0' && ch <= '9') {  // ch 是數字時
    x = x * 10 + (ch - '0');  // 將新讀入的數字’加’在 x 的後面
    // x 是 int 型別,char 型別的 ch 和 ’0’ 會被自動轉為其對應的
    // ASCII 碼,相當於將 ch 轉化為對應數字
    // 此處也可以使用 (x<<3)+(x<<1) 的寫法來代替 x*10
    ch = getchar();  // 繼續讀入
  }
  return x * w;  // 數字 * 正負號 = 實際數值
}
  • 舉例

讀入 num 可寫為 num=read();

輸出優化

原理

同樣是眾所周知, putchar 是用來輸出單個字元的函式

因此將數字的每一位轉化為字元輸出以加速

要注意的是,負號要單獨判斷輸出,並且每次 %(mod)取出的是數字末位,因此要倒序輸出

程式碼實現

int write(int x) {
  if (x < 0) {  // 判負 + 輸出負號 + 變原數為正數
    x = -x;
    putchar('-');
  }
  if (x > 9) write(x / 10);  // 遞迴,將除最後一位外的其他部分放到遞迴中輸出
  putchar(x % 10 + '0');  // 已經輸出(遞迴)完 x 末位前的所有數字,輸出末位
}

但是遞迴實現常數是較大的,我們可以寫一個棧來實現這個過程

inline void write(int x) {
  static int sta[35];
  int top = 0;
  do {
    sta[top++] = x % 10, x /= 10;
  } while (x);
  while (top) putchar(sta[--top] + 48);  // 48 是 '0'
}
  • 舉例

輸出 num 可寫為 write(num);

更快的讀入/輸出優化

通過 fread 或者 mmap 可以實現更快的讀入。其本質為一次性將輸入檔案讀入一個巨大的快取區,如此比逐個字元讀入要快的多 ( getchar , putchar )。因為硬碟的多次讀寫速度是要慢於記憶體的,所以先一次性讀到快取區裡再從快取區讀入要快的多。

更通用的是 fread ,因為 mmap 不能在 Windows 環境下使用。

fread 類似於引數為 "%s"scanf ,不過它更為快速,而且可以一次性讀入若干個字元(包括空格換行等製表符),如果快取區足夠大,甚至可以一次性讀入整個檔案。

對於輸出,我們還有對應的 fwrite 函式

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);

使用示例: fread(Buf, 1, SIZE, stdin) ,表示從 stdin 檔案流中讀入 SIZE 個大小為 1 byte 的資料塊到 Buf 中。

讀入之後的使用就跟普通的讀入優化相似了,只需要重定義一下 getchar。它原來是從檔案中讀入一個 char,現在變成從 Buf 中讀入一個 char,也就是頭指標向後移動一位。

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

fwrite 也是類似的,先放入一個 OutBuf[MAXSIZE] 中,最後通過 fwrite 一次性將 OutBuf 輸出。

參考程式碼:

namespace IO {
const int MAXSIZE = 1 << 20;
char buf[MAXSIZE], *p1, *p2;
#define gc()                                                               \
  (p1 == p2 && (p2 = (p1 = buf) + fread(buf, 1, MAXSIZE, stdin), p1 == p2) \
       ? EOF                                                               \
       : *p1++)
inline int rd() {
  int x = 0, f = 1;
  char c = gc();
  while (!isdigit(c)) {
    if (c == '-') f = -1;
    c = gc();
  }
  while (isdigit(c)) x = x * 10 + (c ^ 48), c = gc();
  return x * f;
}
char pbuf[1 << 20], *pp = pbuf;
inline void push(const char &c) {
  if (pp - pbuf == 1 << 20) fwrite(pbuf, 1, 1 << 20, stdout), pp = pbuf;
  *pp++ = c;
}
inline void write(int x) {
  static int sta[35];
  int top = 0;
  do {
    sta[top++] = x % 10, x /= 10;
  } while (x);
  while (top) push(sta[--top] + '0');
}
}  // namespace IO

輸入輸出的緩衝

printfscanf 是有緩衝區的。這也就是為什麼,如果輸入函式緊跟在輸出函式之後/輸出函式緊跟在輸入函式之後可能導致錯誤。

重新整理緩衝區

  1. 程式結束
  2. 關閉檔案
  3. printf 輸出 \r 或者 \n 到終端的時候(注:如果是輸出到檔案,則不會重新整理緩衝區)
  4. 手動 fflush()
  5. 緩衝區滿自動重新整理
  6. cout 輸出 endl

使輸入輸出優化更為通用

如果你的程式使用多個型別的變數,那麼可能需要寫多個輸入輸出優化的函式。下面給出的程式碼使用 C++ 中的 template 實現了對於所有整數型別的輸入輸出優化。

template <typename T>
inline T
read() {  //宣告 template 類,要求提供輸入的型別T,並以此型別定義行內函數 read()
  T sum = 0, fl = 1;  //將 sum,fl 和 ch 以輸入的型別定義
  int ch = getchar();
  for (; !isdigit(ch); ch = getchar())
    if (ch == '-') fl = -1;
  for (; isdigit(ch); ch = getchar()) sum = sum * 10 + ch - '0';
  return sum * fl;
}

如果要分別輸入 int 型別的變數 a, long long 型別的變數 b 和 __int128 型別的變數 c,那麼可以寫成

a = read<int>();
b = read<long long>();
c = read<__int128>();

完整帶除錯版

關閉除錯開關時使用 fread() , fwrite() ,退出時自動析構執行 fwrite()

開啟除錯開關時使用 getchar() , putchar() ,便於除錯。

若要開啟檔案讀寫時,請在所有讀寫之前加入 freopen()

// #define DEBUG 1  //除錯開關
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
  inline char gc() {
#if DEBUG  //除錯,可顯示字元
    return getchar();
#endif
    if (p1 == p2) p2 = (p1 = buf) + fread(buf, 1, MAXSIZE, stdin);
    return p1 == p2 ? ' ' : *p1++;
  }
  inline bool blank(char ch) {
    return ch == ' ' || ch == '\n' || ch == '\r' || ch == '\t';
  }
  template <class T>
  inline void read(T &x) {
    register double tmp = 1;
    register bool sign = 0;
    x = 0;
    register char ch = gc();
    for (; !isdigit(ch); ch = gc())
      if (ch == '-') sign = 1;
    for (; isdigit(ch); ch = gc()) x = x * 10 + (ch - '0');
    if (ch == '.')
      for (ch = gc(); isdigit(ch); ch = gc())
        tmp /= 10.0, x += tmp * (ch - '0');
    if (sign) x = -x;
  }
  inline void read(char *s) {
    register char ch = gc();
    for (; blank(ch); ch = gc())
      ;
    for (; !blank(ch); ch = gc()) *s++ = ch;
    *s = 0;
  }
  inline void read(char &c) {
    for (c = gc(); blank(c); c = gc())
      ;
  }
  inline void push(const char &c) {
#if DEBUG  //除錯,可顯示字元
    putchar(c);
#else
    if (pp - pbuf == MAXSIZE) fwrite(pbuf, 1, MAXSIZE, stdout), pp = pbuf;
    *pp++ = c;
#endif
  }
  template <class T>
  inline void write(T x) {
    if (x < 0) x = -x, push('-');  // 負數輸出
    static T sta[35];
    T top = 0;
    do {
      sta[top++] = x % 10, x /= 10;
    } while (x);
    while (top) push(sta[--top] + '0');
  }
  template <class T>
  inline void write(T x, char lastChar) {
    write(x), push(lastChar);
  }
} io;

參考

http://www.hankcs.com/program/cpp/cin-tie-with-sync_with_stdio-acceleration-input-and-output.html

http://meme.biology.tohoku.ac.jp/students/iwasaki/cxx/speed.html

相關文章