【问题标题】:Why this method using putchar_unlocked is slower than printf and cout to print strings?为什么这种使用 putchar_unlocked 的方法比 printf 和 cout 打印字符串要慢?
【发布时间】:2015-12-16 19:45:22
【问题描述】:

我正在研究加速我的代码以用于编程比赛的方法,用作输入和输出处理的基础加速。

我目前正在使用线程不安全的putchar_unlocked 函数来打印一些测试。我相信如果由于其线程可解锁的特性而得到良好实现,该函数对于某些数据类型比 cout e printf 更快。​​

我实现了一个以这种方式打印字符串的函数(在我看来非常简单):

void write_str(char s[], int n){
    int i;
    for(i=0;i<n;i++)
        putchar_unlocked(s[i]);
}

我使用大小为 n 且正好为 n 个字符的字符串进行了测试。
但它是三个中最慢的,我们可以在这张输出写入数量与时间(以秒为单位)的关系图中看到:

为什么它最慢?

【问题讨论】:

  • y 轴 = 时间(秒); x 轴 = 写入次数
  • 如何获得n?硬编码常量?还是使用strlen()?另外,为什么不使用fputs()fwrite()
  • @DietmarKühl 实际上图表的标题很清楚(虽然它是葡萄牙语)。它读取“写入 N 个字符数组的时间”,所以我会说 N 是字符串的数量。
  • 另一个愚蠢的问题:我假设你用优化编译?
  • 这不就是“上下文切换次数”那么简单吗?

标签: c++ c printf cout putchar


【解决方案1】:

我个人的猜测是 printf() 是分块执行的,并且只需要偶尔为每个块传递应用程序/内核边界。

putchar_unlocked() 为每个写入的字节执行此操作。

【讨论】:

  • 不是stdout 行缓冲(用于终端)或块缓冲,除非您另有配置?
【解决方案2】:

假设最多约 1,000,000 百万个字符的时间测量值低于测量阈值,并且对std::coutstdout 的写入是使用使用批量写入的表单(例如std::cout.write(str, size))进行的,我猜putchar_unlock() 除了放置字符之外,大部分时间实际上都在更新数据结构的某些部分。其他批量写入会将数据批量复制到缓冲区中(例如,使用memcpy())并在内部仅更新一次数据结构。

也就是说,代码看起来像这样(这是 pidgeon 代码,即,只是粗略地显示正在发生的事情;真正的代码至少会稍微复杂一些):

int putchar_unlocked(int c) {
    *stdout->put_pointer++ = c;
    if (stdout->put_pointer != stdout->buffer_end) {
        return c;
    }
    int rc = write(stdout->fd, stdout->buffer_begin, stdout->put_pointer - stdout->buffer_begin);
    // ignore partial writes
    stdout->put_pointer = stdout->buffer_begin;
    return rc == stdout->buffer_size? c: EOF;
}

代码的批量版本是在做一些事情(使用 C++ 符号,因为作为 C++ 开发人员更容易;再次,这是 pidgeon 代码):

int std::streambuf::write(char const* s, std::streamsize n) {
    std::lock_guard<std::mutex> guard(this->mutex);
    std::streamsize b = std::min(n, this->epptr() - this->pptr());
    memcpy(this->pptr(), s, b);
    this->pbump(b);
    bool success = true;
    if (this->pptr() == this->epptr()) {
        success = this->this->epptr() - this->pbase()
            != write(this->fd, this->pbase(), this->epptr() - this->pbase();
        // also ignoring partial writes
        this->setp(this->pbase(), this->epptr());
        memcpy(this->pptr(), s + b, n - b);
        this->pbump(n - b);
    }
    return success? n: -1;
}

第二个代码可能看起来有点复杂,但只执行一次 30 个字符。很多检查都移出了有趣的部分。即使完成了一些锁定,它也锁定了一个非竞争互斥体,不会过多地抑制处理。

特别是当不使用putchar_unlocked() 对循环进行任何分析时,不会得到太多优化。特别是,代码不会被矢量化,这会导致直接因子至少约为 3,但在实际循环中可能更接近 16。锁的成本会很快减少。

顺便说一句,只是为了创建合理水平的游乐场:除了优化之外,您还应该在使用 C++ 标准流对象时调用 std::sync_with_stdio(false)

【讨论】:

    【解决方案3】:

    选择更快的方式来输出字符串会与平台、操作系统、编译器设置和使用的运行时库发生冲突,但有一些概括可能有助于理解选择什么。

    首先,考虑到操作系统可能有一种显示字符串的方法,而不是一次显示一个字符,如果是这样,循环通过系统调用一次输出一个字符自然会为每次调用系统,而不是一个系统调用处理字符数组的开销。

    这基本上就是您遇到的,系统调用的开销。

    与 putchar 相比,putchar_unlocked 的性能提升可能相当可观,但仅限于这两个函数之间。此外,大多数运行时库都没有 putchar_unlocked(我在较旧的 MAC OS X 文档中找到它,但在 Linux 或 Windows 中没有)。

    也就是说,无论是锁定还是解锁,对于处理整个字符数组的系统调用可能会消除每个字符的开销,并且此类概念扩展到输出到文件或其他设备,而不仅仅是控制台。

    【讨论】:

    • 嗨@Jvene 这个开销也出现在输入中吗?因为 getchar_unlocked 在输入处理的所有情况下都赢得了 scanfcin 。我正在使用 debian 测试和 gcc 5.2.1(也是 g++)。
    • 从概念上讲,scanf 有很多工作要做,而 getchar(任何风格)没有。查看 scanf 的源代码以了解原因。此外,上下文很重要,以及与 cin 相比,您如何评估 getchar。您是否在命令行从管道中获取输入?如果是这样,使用什么字符串处理?您可能会发现 cin 的字符串处理职责比您使用 getchar 定义的要多。也就是说,调用 OUTPUT 函数的上下文与调用 INPUT 函数的上下文完全不同,尤其是像 scanf 这样的函数。
    猜你喜欢
    • 2020-11-08
    • 2017-06-02
    • 1970-01-01
    • 1970-01-01
    • 2013-08-12
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多