【问题标题】:Fast I/O in c, stdin/outc 中的快速 I/O,标准输入/输出
【发布时间】:2017-09-20 05:58:41
【问题描述】:

this link 指定的编码竞赛中,您需要读取stdin 上的大量数据,进行一些计算并在stdout 上呈现大量数据。

在我的基准测试中,尽管我已尝试尽可能优化它,但几乎只有 i/o 需要时间。

您的输入是一个字符串 (1 <= len <= 100'000) 和 q 行 int 对,其中 q 也是 1 <= q <= 100'000

我在一个大 100 倍的数据集(len = 10M,q = 10M)上对我的代码进行了基准测试,结果如下:

 Activity            time      accumulated

 Read text:          0.004     0.004
 Read numbers:       0.146     0.150
 Parse numbers:      0.200     0.350
 Calc answers:       0.001     0.351
 Format output:      0.037     0.388
 Print output:       0.143     0.531

通过实现我自己的内联格式和数字解析,我设法将使用 printfscanf 的时间缩短到了 1/3。

但是,当我将解决方案上传到比赛网页时,我的解决方案耗时 1.88 秒(我认为这是 22 个数据集的总时间)。当我查看高分时,有几个实现(在 c++ 中)在 0.05 秒内完成,比我的快近 40 倍!这怎么可能?

我想我可以通过使用 2 个线程来加快速度,然后我可以开始计算并写入标准输出,同时仍然从标准输入读取。然而,在我的大型数据集的理论上最好的情况下,这将减少到 min(0.150, 0.143) 的时间。我离高分还差得很远。。

在下图中你可以看到消耗时间的统计数据。

网站使用以下选项编译程序:

gcc -g -O2 -std=gnu99 -static my_file.c -lm

时间是这样的:

time ./a.out < sample.in > sample.out

我的代码如下所示:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_LEN (100000 + 1)
#define ROW_LEN (6 + 1)
#define DOUBLE_ROW_LEN (2*ROW_LEN)

int main(int argc, char *argv[])
{
    int ret = 1;

    // Set custom buffers for stdin and out
    char stdout_buf[16384];
    setvbuf(stdout, stdout_buf, _IOFBF, 16384);
    char stdin_buf[16384];
    setvbuf(stdin, stdin_buf, _IOFBF, 16384);

    // Read stdin to buffer
    char *buf = malloc(MAX_LEN);
    if (!buf) {
        printf("Failed to allocate buffer");
        return 1;
    }
    if (!fgets(buf, MAX_LEN, stdin))
        goto EXIT_A;

    // Get the num tests
    int m ;
    scanf("%d\n", &m);

    char *num_buf = malloc(DOUBLE_ROW_LEN);
    if (!num_buf) {
        printf("Failed to allocate num_buffer");
        goto EXIT_A;
    }

    int *nn;
    int *start = calloc(m, sizeof(int));
    int *stop = calloc(m, sizeof(int));
    int *staptr = start; 
    int *stpptr = stop;
    char *cptr;
    for(int i=0; i<m; i++) {
        fgets(num_buf, DOUBLE_ROW_LEN, stdin);
        nn = staptr++;
        cptr = num_buf-1;
        while(*(++cptr) > '\n') {
            if (*cptr == ' ')
                nn = stpptr++;
            else
                *nn = *nn*10 + *cptr-'0';
        }
    }


    // Count for each test
    char *buf_end = strchr(buf, '\0');
    int len, shift;
    char outbuf[ROW_LEN];
    char *ptr_l, *ptr_r, *out;
    for(int i=0; i<m; i++) {
        ptr_l = buf + start[i];
        ptr_r = buf + stop[i];
        while(ptr_r < buf_end && *ptr_l == *ptr_r) {
            ++ptr_l;
            ++ptr_r;
        }

        // Print length of same sequence
        shift = len = (int)(ptr_l - (buf + start[i]));
        out = outbuf;
        do {
            out++;
            shift /= 10;
        } while (shift);
        *out = '\0';
        do {
            *(--out) = "0123456789"[len%10];
            len /= 10;
        } while(len);
        puts(outbuf);
    }



    ret = 0;

    free(start);
    free(stop);
EXIT_A:
    free(buf);
    return ret;
}

【问题讨论】:

  • 为什么要为单个整数分配内存?你在什么系统上?在 Linux 上,stdio 更快(并且比 Windows 上的 iostream 更快),在 Windows 上,iostream 胜过 stdio。 stdio 可以通过使用 IO 函数的未锁定变体(puts_unlocked 而不是 puts 等)来加快速度,因为 POSIX 要求 stdio 对调用使用递归锁,而 iostream (AFAIK) 不存在这样的要求。
  • 看起来你每次都在循环输出。如果你用内存换取速度,分配一个更大的缓冲区,然后一次打印整个输出呢?或者,如果输出太多而无法实现,您仍然可以通过缓冲来充分整合输出。如果puts 实际上 是您的瓶颈,这将解决问题。我不确定你是如何衡量到那个时候到达的。例如,“打印输出”测量包括哪些所有操作?
  • 次要:cptr = num_buf-1; 是未定义的行为 - 尽管它可能按需要“工作”。
  • 问题说明了每个数组的最大大小,因此消除对malloc()calloc() 的调用,只需声明数组足够大以容纳最大数据量
  • 强烈建议不要打扰setvbuf()

标签: c performance optimization stdout stdin


【解决方案1】:

您应该连续分配所有缓冲区。 分配一个与所有缓冲区(num_buff、start、stop)大小相同的缓冲区,然后将这些点重新排列到相应的偏移量上。 这可以减少您的缓存未命中\页面错误。

由于读取和写入操作似乎消耗大量时间,您应该考虑添加线程。一个线程应该处理 I\O,另一个应该处理计算。 (值得检查另一个打印线程是否也可以加快速度)。确保在执行此操作时不要使用任何锁。

【讨论】:

    【解决方案2】:

    回答这个问题很棘手,因为优化很大程度上取决于您遇到的问题。 一个想法是查看您尝试阅读的文件的内容,看看是否有您可以使用的模式或东西。 您编写的代码是从文件中读取、执行某些内容然后写入文件的“通用”解决方案。但是,如果您的文件不是每次都随机生成并且内容始终相同,为什么不尝试为该文件编写解决方案呢?

    另一方面,您可以尝试使用低级系统函数。我想到的是mmap,它允许您将文件直接映射到内存并访问该内存,而不是使用scanffgets

    我发现可能有帮助的另一件事是在您的解决方案中,您有两个 while 循环,为什么不尝试只使用一个呢?另一件事是进行一些异步 I/O 读取,因此不是在循环中读取整个文件,然后在另一个循环中进行计算,您可以尝试在开始时读取一部分,开始异步处理并继续读。 这个link 可能对异步部分有所帮助

    【讨论】:

      【解决方案3】:

      多亏了你的问题,我自己去解决了这个问题。你的时间比我的好,但我仍在使用一些 stdio 功能。

      我根本不认为 0.05 秒的高分是真实的。我怀疑这是一个高度自动化的系统的产物,它返回了错误的结果,并且没有人验证过它。

      如何为这种说法辩护?没有真正的算法复杂性:问题是O(n)。 “技巧”是为输入的每个方面编写专门的解析器(并避免仅在调试模式下完成的工作)。 22 次试验的总时间为 50 毫秒,意味着每次试验平均为 2.25 毫秒?我们已经接近可衡量的门槛。

      在某种程度上,像你自己解决的问题这样的竞争是不幸的。他们强化了这样一种天真的想法,即性能是程序的最终衡量标准(没有分数是为了清晰)。更糟糕的是,他们鼓励“为了性能”而绕过诸如 scanf 之类的事情,而在现实生活中,让程序正确快速地运行基本上不需要避免甚至调整 stdio。在复杂系统中,性能来自诸如避免 I/O、仅传递数据一次以及最小化副本等因素。有效地使用 DBMS 通常是关键(事实上),但这样的事情从未出现在编程挑战中。

      将数字解析和格式化为文本确实需要时间,并且在极少数情况下可能会成为瓶颈。但答案几乎永远不会重写解析器。相反,答案是将文本解析为方便的二进制形式,并使用它。简而言之:编译。

      也就是说,一些观察可能会有所帮助。

      你不需要动态内存来解决这个问题,它没有帮助。问题陈述说输入数组可能多达 100,000 个元素,试验次数可能多达 100,000 个。每个试验是两个整数字符串,最多 6 位,每个由空格分隔并以换行符结尾:6 + 1 + 6 + 1 = 14。总输入,最大值为 100,000 + 1 + 6 + 1 + 100,000 * 14:下16 KB。您可以使用 1 GB 的内存。

      我刚刚分配了一个 16 KB 的缓冲区,并使用 read(2) 一次性读取它。然后我对该输入进行了一次传递。

      您收到了使用异步 I/O 和线程的建议。问题陈述说你是根据 CPU 时间来衡量的,所以这些都没有帮助。两点之间最短的距离是直线;单次读取静态分配的内存不会浪费任何动作。

      他们衡量性能的方式的一个荒谬的方面是他们使用 gcc -g。这意味着 assert(3) 在代码中被调用以衡量性能!在我删除我的断言之前,我在测试 22 中的时间不能低于 4 秒。

      总之,你做得很好,我怀疑你所困惑的获胜者是一个幻影。您的代码确实有点麻烦,您可以省去动态内存和调整 stdio。我打赌你的时间可以通过简化来减少。就性能而言,这就是我要引起您注意的地方。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2012-01-20
        • 1970-01-01
        • 2021-08-17
        • 1970-01-01
        • 2011-12-20
        相关资源
        最近更新 更多