简体   繁体   中英

Why is my line count function written in C slower than Ruby?

I have two implementations of a function that prints the number of lines in a given file, one written in C and the other in Ruby. For some strange reason, the Ruby version is 2x faster! Here's the code:

linecount.c (compiled with gcc linecount.c -o linecount )

#include <stdio.h>

int main(int argc, char **argv) {
  FILE *fp;
  int c;
  int count;

  fp = fopen(argv[1], "r");

  while ((c = getc(fp)) != EOF) {
    if (c == '\n') {
      count++;
    }
  }

  fclose(fp);
  printf("%d\n", count);
  return 0;
}

ruby_linecount.rb

#!/usr/bin/env ruby

puts File.open(ARGV[0]).lines.count

These are the benchmarks:

time (for i in {1..100}; do ./linecount /usr/share/dict/words; done)
real    0m14.438s 
user    0m14.041s
sys     0m0.298s

time (for i in {1..100}; do ./ruby_linecount.rb /usr/share/dict/words; done)
real    0m6.910s
user    0m5.917s
sys     0m0.734s

Why is the C version so much slower? How can I improve the performance of the C code? Are there any compiler flags that would help?

You can use compiler option -O3 to optimize for performance. Also you may consider using fgets to avoid reading the file character by character.

Reading from a file is a high-latency operation. Most likely, the speed of the C version could be increased by reading larger blocks of data from the file.

I offer two examples.

This first one uses a 16K buffer. The buffer size might be changed to see even better performance.

EXAMPLE 1

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

#define BUFSIZE (16 * 1024)

int main(int argc, char **argv)
   {
   int rCode;
   FILE *fp = NULL;
   char *buf = NULL;
   int count = 0;
   size_t bufLen;

   errno=0;
   fp = fopen(argv[1], "r");
   if(NULL == fp)
      {
      rCode=errno;
      fprintf(stder, "fopen() failed.  errno:%d\n", errno);
      goto CLEANUP;
      }

   errno=0;
   buf = malloc(BUFSIZE);
   if(NULL == buf)
      {
      rCode=errno;
      fprintf(stder, "malloc() failed.  errno:%d\n", errno);
      goto CLEANUP;
      }

   bufLen = fread(buf, 1, BUFSIZE, fp);
   while(bufLen)
      {
      char *cp;

      for(cp=buf; (cp < buf + bufLen); ++cp)
         if('\n' == *cp)
            ++count;

          bufLen = fread(buf, 1, BUFSIZE, fp);
      }

   printf("%d\n", count);

CLEANUP:

   if(fp)
      fclose(fp);

   if(buf)
      free(buf);

   return(rCode);
   }

This next one maps the file into the process memory map (or address space). Then, looking for new-lines is memory search for newlines operation.

EXAMPLE 2

#include <errno.h>    /* errno, ... */
#include <fcntl.h>    /* open(), O__RDONLY, ... */
#include <stdio.h>    /* fprintf(), stderr, printf(), ... */
#include <sys/mman.h> /* mmap(), PROT_READ, MAP_SHARED, ... */
#include <sys/stat.h> /* fstat(), struct stat, ... */
#include <unistd.h>   /* close(), ... */

int main(int argc, char **argv)
   {
   int rCode=0;
   int fd = (-1);
   struct stat statBuf;
   char *fileBuf=NULL;
   char *cp;
   int   count=0;

   errno=0;
   fd = open(argv[1], O_RDONLY);
   if((-1) == fd)
      {
      rCode=errno;
      fprintf(stderr, "open() failed.  errno:%d\n", errno);
      goto CLEANUP;
      }

   errno=0;
   if((-1) == fstat(fd, &statBuf))
      {
      rCode=errno;
      fprintf(stderr, "fstat() failed.  errno:%d\n", errno);
      goto CLEANUP;
      }

   errno=0;
   fileBuf = mmap(
         NULL,            /* preferred start address, normally NULL (system chooses) */
         statBuf.st_size, /* length of the mapped region */
         PROT_READ,       /* memory protection */
         MAP_SHARED,      /* private/shared */
         fd,              /* fd of mapped file */
         0                /* file offset (should be a multiples of a page) */
         );
   if((void *)(-1) == fileBuf)
      {
      rCode=errno;
      fprintf(stderr, "mmap() failed.  errno:%d\n", errno);
      goto CLEANUP;
      }

   for(cp=fileBuf; cp < fileBuf + statBuf.st_size; ++cp)
      if('\n' == *cp)
         ++count;

  printf("%d\n", count);

CLEANUP:

   if(fileBuf)
      munmap(fileBuf, statBuf.st_size);

   if((-1) != fd)
      close(fd);

   return(rCode);
   }

EDIT

I agree with the Neil Slater's comment. While the examples above should improve the speed of the operation (compared to the example in the question code); Perhaps Ruby will be just a fast.

Using your code on a MacBook Pro shows wrong results from the C version. I'd suggest changing the while loop to this:

 while ( (c=fgetc(fp)) != EOF )

using fgetc instead of getc . Moreover you can optimize the compiler with the -O3 flag as previously suggested.

For the ruby code, it's better to use each_line instead of lines (it's a deprecated alias).

Depending on the ruby version you are using, you can get different results. On my machine, ruby is about 50 times slower than the corresponding C code.

What you're trying to do should be heavily IO-bound. In other words, most of the time is spent reading the file rather than doing actual computation. You should try to use another way of reading your file in your C version, and maybe have a look at what Ruby is using.

I'm guessing mmap could give you better performance that getc .

Also, be careful about the order in which you benchmark programs. The file will likely be in memory cache after the first run of one of the programs, making the other one faster. You should run each program multiple times in order to get more accurate timings.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM