没有使用"-std=c99"时,fprintf速度差异巨大的问题

19

我曾经苦于一位我自己写的翻译器表现不佳已经好几个星期了。 在下面这个简单的基准测试中

#include<stdio.h>

int main()
{
    int x;
    char buf[2048];
    FILE *test = fopen("test.out", "wb");
    setvbuf(test, buf, _IOFBF, sizeof buf);
    for(x=0;x<1024*1024; x++)
        fprintf(test, "%04d", x);
    fclose(test);
    return 0
}

我们看到以下结果

bash-3.1$ gcc -O2 -static test.c -o test
bash-3.1$ time ./test

real    0m0.334s
user    0m0.015s
sys     0m0.016s

正如您所看到的,一旦加入“-std=c99”标志,性能便会急剧下降:

bash-3.1$ gcc -O2 -static -std=c99 test.c -o test
bash-3.1$ time ./test

real    0m2.477s
user    0m0.015s
sys     0m0.000s

我正在使用的编译器是gcc 4.6.2 mingw32。

生成的文件大约有12M,因此这两者之间存在着21MB/s左右的差异。

运行diff显示生成的文件完全相同。

我认为这与程序中大量使用的fprintf文件锁有关,但我无法找到在C99版本中关闭它的方法。

我尝试在程序开头使用flockfile函数,并在结尾处使用相应的funlockfile函数,但却遇到了隐式声明的编译器错误以及链接器错误声称这些函数未定义。

除了文件锁,还有其他可能导致这个问题的原因吗?更重要的是,在Windows上是否有使用C99而不付出如此巨大性能代价的方法?


编辑:

查看这些选项生成的代码后,发现在慢速版本中,mingw会插入以下内容:

_fprintf:
LFB0:
    .cfi_startproc
    subl    $28, %esp
    .cfi_def_cfa_offset 32
    leal    40(%esp), %eax
    movl    %eax, 8(%esp)
    movl    36(%esp), %eax
    movl    %eax, 4(%esp)
    movl    32(%esp), %eax
    movl    %eax, (%esp)
    call    ___mingw_vfprintf
    addl    $28, %esp
    .cfi_def_cfa_offset 4
    ret
    .cfi_endproc 
在快速版本中,这个东西就不存在了;否则,两者完全相同。我认为__mingw_vfprintf似乎是这里的拖累,但我不知道它需要模仿什么行为才能使其如此缓慢。
在快速版本中,这个东西就不存在;否则,两者完全相同。我认为__mingw_vfprintf似乎是这里的拖累,但我不知道它需要模仿什么行为才能使其如此缓慢。

1
你需要将问题隔离到一个完整的工作示例代码中,以便你可以实际发布的代码,让人们猜测你可能做了什么是没有多大帮助的。 - unwind
1
注释掉printf,然后重新测试以查看它是否是差异的真正源头。 - Mat
1
@unwind,我道歉;我非常确信问题出在printf上,以至于我犯了这个网站的基本规则。我甚至没有想到问题可能出在其他地方。 - Dave
好的,所以在快速版本中 _fprintf 不存在。那么,主循环是什么样子的呢? - rodrigo
@rodrigo 快的那个没有重新定义 fprintf,主循环是相同的。我现在正在尝试查看mingw源代码,看看是否有任何瓶颈。 - Dave
显示剩余3条评论
4个回答

12

经过查找源代码,我发现为什么MinGW函数如此缓慢:

在MinGW中的[v,f,s]printf开始时,有一些看似无害的初始化代码:

__pformat_t stream = {
    dest,                   /* output goes to here        */
    flags &= PFORMAT_TO_FILE | PFORMAT_NOLIMIT, /* only these valid initially */
    PFORMAT_IGNORE,             /* no field width yet         */
    PFORMAT_IGNORE,             /* nor any precision spec     */
    PFORMAT_RPINIT,             /* radix point uninitialised  */
    (wchar_t)(0),               /* leave it unspecified       */
    0,                          /* zero output char count     */
    max,                        /* establish output limit     */
    PFORMAT_MINEXP          /* exponent chars preferred   */
};

然而,PFORMAT_MINEXP并不是表面上看起来的那样:

#ifdef _WIN32
# define PFORMAT_MINEXP    __pformat_exponent_digits() 
# ifndef _TWO_DIGIT_EXPONENT
#  define _get_output_format()  0 
#  define _TWO_DIGIT_EXPONENT   1
# endif
static __inline__ __attribute__((__always_inline__))
int __pformat_exponent_digits( void )
{
  char *exponent_digits = getenv( "PRINTF_EXPONENT_DIGITS" );
  return ((exponent_digits != NULL) && ((unsigned)(*exponent_digits - '0') < 3))
    || (_get_output_format() & _TWO_DIGIT_EXPONENT)
    ? 2
    : 3
    ;
}

每次我想打印时,这个函数都会被调用,而在Windows上使用getenv可能不是很快。将该定义替换为2可以将运行时间恢复到正常水平。


因此,答案归结为:当使用-std=c99或任何符合ANSI标准的模式时,MinGW会切换CRT运行时为自己的运行时。通常,这不会是一个问题,但MinGW库存在一个错误,使其格式化函数的速度远远慢于可想象的任何情况。


你可能需要向mingw32开发人员报告一个bug。 - Yann Droneaud
我打算着手准备一个补丁,希望如果我已经有了解决方案,他们会更加接受。 - Dave
这个行为在我在其他答案中链接的更改日志中有记录。 - Yann Droneaud
getenv() 如果在运行时初始化时只执行一次,那么很可能是可以的。 - Yann Droneaud
但事实并非如此。每次调用格式化函数时都会执行它。它应该是 pre-main libc init goo 中的钩子。 - Dave
显示剩余2条评论

8

那么`-std=gnu99'会是一个公平的比较,不是吗? - rodrigo
3
情节变得扑朔迷雾:gnu99 很快,但 fprintf(test, "%04d\n", x)gnu99 上仍然很快,在 c99 上却很慢。 - Dave
3
不需要拆卸,只需运行 gcc -std=c99 -O2 -S file.c,它会生成 file.s 汇编文件。您也可以使用 gcc -v -std=c99 -O2 file.c -o file 检查链接器参数。@Dave @rodrigo - Yann Droneaud
2
@ydroneaud - 是的,但我发现objdump -S更容易阅读(因为汇编与源代码交织在一起)。无论如何,我已经使用"%04d",GCC 4.7.2进行了c99 vs. gnu99 vs nothing的测试,并且没有可测量的性能差异。 - rodrigo
@rodrigo @peter-schneider 指定 -std= 等同于 -std=gnu99 - Yann Droneaud
显示剩余2条评论

0
经过对您的汇编程序的一些考虑,看起来慢版本正在使用MinGW的*printf()实现,无疑是基于GCC的实现,而快速版本则使用了来自msvcrt.dll的Microsoft实现。
现在,MS的实现明显缺少许多功能,而GCC的实现则实现了这些功能。其中一些是GNU扩展,但其他一些是为了符合C99标准。由于您正在使用-std=c99,因此您正在请求符合性。
但为什么会这么慢呢?嗯,一个因素是简单性,MS版本要简单得多,因此预计它将在微不足道的情况下运行得更快。另一个因素是您正在Windows下运行,因此预计MS版本比从Unix世界复制的版本更有效率。
这能解释10倍的因素吗?可能不是...
您可以尝试另一件事:
  • sprintf()替换fprintf(),将数据打印到内存缓冲区中而不触及文件。然后,您可以尝试使用fwrite()而不是printfing。这样,您就可以猜测数据格式化或写入FILE中的损失。

0
自从MinGW32 3.15版本以来,可以使用符合标准的printf函数,而不是Microsoft C运行时(CRT)中的函数。 在严格的ANSI、POSIX和/或C99模式下编译时,将使用新的printf函数。
有关更多信息,请参见mingw32 changelog
您可以使用__msvcrt_fprintf()来使用快速(非符合标准)函数。

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