fprintf与std::ofstream的性能非常出乎意料(fprintf速度非常慢)

9
我正在进行一些基准测试,以找到在C++中将大型数组(超过1Go的ASCII字符)写入文件的最高效方式。 因此,我比较了std :: ofstream与fprintf(请参见我使用的开关)。
    case 0: {
        std::ofstream out(title, std::ios::out | std::ios::trunc);
        if (out) {
            ok = true;
            for (i=0; i<M; i++) {
                for (j=0; j<N; j++) {
                    out<<A[i][j]<<" ";
                }
                out<<"\n";
            }
            out.close();
        } else {
            std::cout<<"Error with file : "<<title<<"\n";
        }
        break;
    }
    case 1: {
        FILE *out = fopen(title.c_str(), "w");
        if (out!=NULL) {
            ok = true;
            for (i=0; i<M; i++) {
                for (j=0; j<N; j++) {
                    fprintf(out, "%d ", A[i][j]);
                }
                fprintf(out, "\n");
            }
            fclose(out);
        } else {
            std::cout<<"Error with file : "<<title<<"\n";
        }
        break;
    }

我的一个大问题是,相比于std :: ofstream,fprintf似乎要慢12倍以上。你有没有想过这个问题在我的代码中的根源是什么?或者说,与fprintf相比,std :: ofstream非常优化了吗?
(另一个问题:您知道另一种更快的编写文件的方法吗?)
非常感谢。
(详细信息:我正在使用g ++ -Wall -O3进行编译)

1
请查看ostream::write():http://www.cplusplus.com/reference/iostream/ostream/write/ - Nim
1
@AndersK:不是。fputs是流缓冲区(未格式化)的等效物;fprintf才是ostream的正确对应物。 - MSalters
我想知道每种情况下输出缓冲区的大小是多少。 - Robᵩ
嗨,文森特。我有两个建议,可以提高你收到的答案质量。1)请回到以前的问题,并接受你认为最有用的答案。2)请创建一个最小化、完整的程序,我们可以编译和运行,以演示你的问题。如果我们运行这个程序片段,我们必须做出关于程序其余部分的假设。请参阅http://sscce.org/。 - Robᵩ
4
这个程序源自于OP的代码片段,链接为http://ideone.com/aZW3v。不论执行switch语句中的哪个分支,该程序的运行时间基本相同。FWIW. - Robᵩ
显示剩余3条评论
5个回答

18

fprintf("%d" 需要在运行时每个整数解析一次格式字符串。 ostream& operator<<(ostream&, int) 在编译时仅需要编译器解析一次。


4

嗯,fprintf()在运行时需要进行一些额外的工作,因为它必须解析和处理格式字符串。然而,考虑到您的输出文件的大小,我预计这些差异对结果影响不大,并且代码应该是I/O限制的。

因此,我怀疑您的基准测试存在某种缺陷。

  1. 如果您反复运行测试,是否始终会得到12倍的差异?
  2. 如果您更改运行测试的顺序,时间会发生什么变化?
  3. 如果您在结尾处调用fsync()/sync(),会发生什么情况?

2

在ofstream中有一个文件缓冲区,这可以减少访问磁盘的次数。此外,fprintf是一个具有可变参数的函数,它将调用一些va_#函数,但ofstream不会。我认为你可以使用fwrite()或putc()进行测试。


putc 会更慢,因为它只写入一个字符,所以速度会变慢。 - Salvatore Previti

1
我在这里介绍一种使用Unix函数open、read和write在文本文件中编写整数的真正优化方法。它们也适用于Windows,只是提醒您可以使用。
此实现仅适用于32位整数。
在您的包含文件中:
class FastIntegerWriter
{
private:

    const int bufferSize;
    int offset;
    int file;
    char* buffer;

public:

    FastIntegerWriter(int bufferSize = 4096);
    int Open(const char *filename);
    void Close();
    virtual ~FastIntegerWriter();
    void Flush();
    void Writeline(int value);
};

在你的源代码文件中
#ifdef _MSC_VER
# include <io.h>
# define open _open
# define write _write
# define read _read
# define close _close
#else
# include <unistd.h>
#endif
#include <fcntl.h>

FastIntegerWriter::FastIntegerWriter(int bufferSize) :
    bufferSize(bufferSize),
    buffer(new char[bufferSize]),
    offset(0),
    file(0)
{
}

int FastIntegerWriter::Open(const char* filename)
{
    this->Close();
    if (filename != NULL)
        this->file = open(filename, O_WRONLY | O_CREAT | O_TRUNC);
    return this->file;
}

void FastIntegerWriter::Close()
{
    this->Flush();
    if (this->file > 0)
    {
        close(this->file);
        this->file = 0;
    }
}

FastIntegerWriter::~FastIntegerWriter()
{
    this->Close();
    delete[] this->buffer;
}

void FastIntegerWriter::Flush()
{
    if (this->offset != 0)
    {
        write(this->file, this->buffer, this->offset);
        this->offset = 0;
    }
}

void FastIntegerWriter::Writeline(int value)
{
    if (this->offset >= this->bufferSize - 12)
    {
        this->Flush();
    }

    // Compute number of required digits

    char* output = this->buffer + this->offset;

    if (value < 0)
    {
        if (value == -2147483648)
        {
            // Special case, the minimum integer does not have a corresponding positive value.
            // We use an hard coded string and copy it directly to the buffer.
            // (Thanks to Eugene Ryabtsev for the suggestion).

            static const char s[] = "-2147483648\n";
            for (int i = 0; i < 12; ++i)
                output[i] = s[i];
            this->offset += 12;
            return;
        }

        *output = '-';
        ++output;
        ++this->offset;
        value = -value;
    }

    // Compute number of digits (log base 10(value) + 1)

    int digits =
        (value >= 1000000000) ? 10 : (value >= 100000000) ? 9 : (value >= 10000000) ? 8 : 
        (value >= 1000000) ? 7 : (value >= 100000) ? 6 : (value >= 10000) ? 5 : 
        (value >= 1000) ? 4 : (value >= 100) ? 3 : (value >= 10) ? 2 : 1;

    // Convert number to string

    output[digits] = '\n';
    for (int i = digits - 1; i >= 0; --i)
    {
        output[i] = value % 10 + '0';
        value /= 10;
    }

    this->offset += digits + 1;
}

我猜这个方法会胜过写入ASCII文件的其他任何方法:) 你可以使用Windows低级API WriteFile和ReadFile来获得更高的性能,但这不值得努力。
要使用它...
int main()
{
    FastIntegerWriter fw;
    fw.Open("test.txt");

    for (int i = -2000; i < 1000000; ++i)
        fw.Writeline(i);

    return 0;
}

如果您没有指定任何文件,则使用标准输出(控制台)。


1
请注意,如果传递的是最小的负整数,则 value = -value 将无法正确工作,因为没有相应的正值。请参阅 http://stackoverflow.com/a/5165813/1353187。 - Eugene Ryabtsev
没错。在编写那段代码时没有考虑到这一点。处理它的最佳和最简单的方法是在一个字符串中硬编码最负整数,并编写一个if (value == most_negative_integer) write_the_string。 - Salvatore Previti
这里展示了更快的实现方式:https://dev59.com/TG855IYBdhLWcg3wbjrO - Ben Voigt

1

你在代码上游设置了sync_with_stdio吗?

虽然你所报告的与实际看到的相反,但大多数人认为你所看到的应该是正常的。iostreams是类型安全的,而printf函数族是可变参数函数,必须从格式说明符中推断出va_list的类型。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接