C比Java慢:为什么?

10

我快速编写了一个C程序,用于提取一组gzipped文件的i-th line(包含约500,000行)。以下是我的C程序:

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

/* compilation:
gcc  -o linesbyindex -Wall -O3 linesbyindex.c -lz
*/
#define MY_BUFFER_SIZE 10000000
static void extract(long int index,const char* filename)
   {
   char buffer[MY_BUFFER_SIZE];
   long int curr=1;
   gzFile in=gzopen (filename, "rb");
   if(in==NULL)
       {
       fprintf(stderr,"Cannot open \"%s\" %s.\n",filename,strerror(errno));
       exit(EXIT_FAILURE);              }
   while(gzread(in,buffer,MY_BUFFER_SIZE)!=-1 && curr<=index)
       {
       char* p=buffer;
       while(*p!=0)
           {
           if(curr==index)
               {
               fputc(*p,stdout);
               }
           if(*p=='\n')
               {
               ++curr;
               if(curr>index) break;
               }
           p++;
           }
       }
   gzclose(in);
   if(curr<index)
       {
       fprintf(stderr,"Not enough lines in %s (%ld)\n",filename,curr);
       }
   }

int main(int argc,char** argv)
   {
   int optind=2;
   char* p2;
   long int count=0;
   if(argc<3)
       {
       fprintf(stderr,"Usage: %s (count) files...\n",argv[0]);
       return EXIT_FAILURE;
       }
   count=strtol(argv[1],&p2,10);
   if(count<1 || *p2!=0)
       {
       fprintf(stderr,"bad number %s\n",argv[1]);
       return EXIT_SUCCESS;
       }
   while(optind< argc)
       {
       extract(count,argv[optind]);
       ++optind;
       }
   return EXIT_SUCCESS;
   } 

作为一个测试,我用Java编写了以下等效代码:

import java.io.*;
import java.util.zip.GZIPInputStream;

public class GetLineByIndex{
   private int index;

   public GetLineByIndex(int count){
       this.index=count;
   }

   private String extract(File file) throws IOException
       {
       long curr=1;
       byte buffer[]=new byte[2048];
       StringBuilder line=null;
       InputStream in=null;
       if(file.getName().toLowerCase().endsWith(".gz")){
           in= (new GZIPInputStream(new FileInputStream(file)));
       }else{
           in= (new FileInputStream(file));
       }
             int nRead=0;
       while((nRead=in.read(buffer))!=-1)
           {
           int i=0;
           while(i<nRead)
               {
               if(buffer[i]=='\n')
                   {
                   ++curr;
                   if(curr>this.index) break;
                                     }
               else if(curr==this.index)
                   {
                   if(line==null) line=new StringBuilder(500);
                   line.append((char)buffer[i]);
                   }
               i++;
               }
           if(curr>this.index) break;
           }
       in.close();
       return (line==null?null:line.toString());
       }

   public static void main(String args[]) throws Exception{
       int optind=1;
       if(args.length<2){
           System.err.println("Usage: program (count) files...\n");
           return;
       }
       GetLineByIndex app=new GetLineByIndex(Integer.parseInt(args[0]));

       while(optind < args.length)
           {
           String line=app.extract(new File(args[optind]));
           if(line==null)
               {
               System.err.println("Not enough lines in "+args[optind]);
               }
           else
               {
               System.out.println(line);
               }
           ++optind;
           }
       return;
   }
} 

在同一台机器上运行测试时,Java程序获取大索引的速度比C程序快得多(约为1分45秒和2分15秒)。我该如何解释这种差异?


2
注意:缓冲区大小不相等,因此程序的功能并不“完全”相同。 - Sani Huttunen
@SaniHuttunen - 这段代码不仅仅因为这个原因而不等价 :) - Perception
@Perception:没错,但这是我的第一次观察,并且足以指出这些程序确实不相等。 - Sani Huttunen
@zr 请查看第一条评论以获取C语言编译结果。 - Pierre
6
也许编译器故意生成低效的代码,因为这种非常不规范的编码风格。如果我是编译器,我也会这样做。 - Lundin
显示剩余5条评论
5个回答

22
Java版本比C版本更快的最有可能的解释是C版本是错误的。
修复了C版本之后,我得到了以下结果(与你声称Java比C更快相矛盾):
Java 1.7 -client: 65 milliseconds (after JVM warmed up)
Java 1.7 -server: 82 milliseconds (after JVM warmed up)
gcc -O3:          37 milliseconds

任务是从文件words.gz中打印第200000行。文件words.gz是通过对/usr/share/dict/words进行gzip压缩生成的。
...
static char buffer[MY_BUFFER_SIZE];
...
ssize_t len;
while((len=gzread(in,buffer,MY_BUFFER_SIZE)) > 0  &&  curr<=index)
    {
    char* p=buffer;
    char* endp=buffer+len;
    while(p < endp)
       {
...

你在 C 版本中做了什么更改? - Pierre
谢谢!第一次编写C代码时,我使用了gzgets而不是gzread,但我没有在循环缓冲区时更改测试。 - Pierre
@Pierre:我明白了。如果你在你的电脑上用你的文件重新运行基准测试,C语言现在比Java语言更快吗? - user811773
是的,我几个小时前在工作中做了测试。但结果的差异没有我预期的那么大。 - Pierre
2
就记录而言,标准的Java Gzip效率低下。 - bestsss
显示剩余5条评论

15

fputc()不太快,而且你是逐个字符地添加输出文件的内容。

调用fputc_unlocked或者将要添加的内容分隔开,并调用fwrite()应该会更快。


你的答案是不正确的。问题的作者没有指定他的GZIP文件中每行的平均长度。 - user811773
fputc()仅在跳过大量类似行之后用于单行。这不是我们应该寻找的内部循环。巨大的自动缓冲区是更好的选择。将其设置为与Java相同的大小(2048)将允许进行公平比较。 - chqrlie

12

你的程序在做不同的事情。我没有对你的程序进行剖析,但从你的代码来看,我怀疑这是不同的地方:

用Java构建该行时,你使用了以下内容:

if(curr==this.index)
{
    if(line==null) line=new StringBuilder(500);
    line.append((char)buffer[i]);
}

在C语言中,可以这样写:

if(curr==index)
{
    fputc(*p,stdout);
}

即:您正在逐个字符地打印到标准输出(stdout)。默认情况下是带缓冲区的,但我怀疑它仍然比您在Java中使用的500个字符缓冲区更慢。


0

0
非常大的缓冲区可能会更慢。我建议您将缓冲区大小设置为相同,即2或8 KB。

我开始使用stdio:BUFSIZ:〜相同的结果 - Pierre
在 C (zlib) 中,大缓存根本不重要,在 Java 中则重要,因为它被多次复制。您可以使用内存映射文件一样。Java 的 FileInputStream 优化了较小的缓冲区 Win 为2K,Linux 为8K,在这种情况下使用堆栈进行分配,否则是 malloc/free(某些 malloc 比 stack 慢得多),这就是为什么较小的缓冲区表现更好的原因。当调用更深层次的递归时,我在本地内存中遭遇了可怕的崩溃,双 SIGSEG,进程挂掉了(第二次发生在尝试写崩溃日志时,因此没有崩溃日志事件) - bestsss

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