Задумавшись однажды над написанием кое-какого приложения у меня возникла нужда считать количество строк в файлах. Считать быстро. А файлы бывают большими (500–1500 Мб). И, поскольку в тот момент я находился на больничном, возник спортивный интерес проверить, какой язык произведёт сие действо за минимальное время.

TL;DR; Go — победил.

Тестовые программы


Итак, были выбраны доступные мне языки:

  • linesCount.c
    #include <stdio.h>
    #include <time.h>
    
    int main(int argc, const char * argv[]) {    
        const char *path = "big.csv";
        
        double startTime = clock();
        
        int linesCount = 0;
        FILE *fp = fopen(path, "r");
        int bufsize = 32*1024;
        char buff[bufsize];
        while (fgets(buff, bufsize, fp) != NULL) {
            linesCount++;
        }
        fclose(fp);
        
        double endTime = clock();
        double totalTime = (endTime - startTime) / CLOCKS_PER_SEC;
        printf("Time: %f s\n", totalTime);
        printf("Lines: %d \n", linesCount);
        
        return 0;
    }
    

  • C# (Mono/macOS, .Net/Windows)
    using System;
    using System.IO;
    
    namespace lineStat
    {
    	class MainClass
    	{
    		public static void Main(string[] args)
    		{
    			var filename = "big.csv";
    
    			var startTime = DateTime.Now;
    			var linesCount = countLinesInFile(filename);
    			var spentTime = DateTime.Now - startTime;
    			Console.WriteLine("Lines count: {0} in {1}", linesCount, spentTime);
    		}
    
    		static int countLinesInFile(string filename)
    		{
    			int linesCount = 0;
    			using (var reader = new StreamReader(filename))
    			{
    				while (reader.ReadLine() != null) {
    					linesCount++;
    				}
    			}
    			return linesCount;
    		}
    	}
    }

  • Go
    package main
    
    import (
    	"bytes"
    	"fmt"
    	"io"
    	"os"
    	"time"
    )
    
    func main() {
    
    	start_time := time.Now()
    	file, _ := os.Open("big.csv")
    	count, _ := lineCounter(file)
    	elapsed := time.Since(start_time)
    	fmt.Printf("Lines count: %d in %s\n", count, elapsed)
    }
    
    func lineCounter(r io.Reader) (int, error) {
    	buf := make([]byte, 32*1024)
    	count := 0
    	lineSep := []byte{'\n'}
    
    	for {
    		c, err := r.Read(buf)
    		count += bytes.Count(buf[:c], lineSep)
    
    		switch {
    		case err == io.EOF:
    			return count, nil
    
    		case err != nil:
    			return count, err
    		}
    	}
    }

  • Java
    package me.meamka;
    
    import java.io.BufferedReader;
    import java.io.FileReader;
    import java.io.IOException;
    import java.text.DecimalFormat;
    import java.text.NumberFormat;
    
    public class LinesCount {
    
        public static void main(String[] args) throws IOException {
    	// write your code here
            String filename = "big.csv";
            System.out.println("Count lines in " + filename);
            long startTime = System.nanoTime();
            int linesCount = countLinesInFile(filename);
            long endTime = System.nanoTime();
            NumberFormat formatter = new DecimalFormat("#0.00000");
            System.out.println("Lines count: " + linesCount +" in " + formatter.format((endTime - startTime) / 1000000000d));
        }
    
        private static int countLinesInFile(String filename) throws IOException {
            int linesCount = 0;
    
            try (BufferedReader br = new BufferedReader(new FileReader(filename))) {
                while (br.readLine() != null) {
                    linesCount++;
                }
            }
            return linesCount;
        }
    }
  • PHP
    <?php
    function countLinesInFile() {
    	$f = fopen('big.csv', 'r');
    	$lineCount = 0;
    	while (!feof($f))
    	{
    		if (fgets($f))
            $lineCount++;
    	}
    	fclose($f);
    	return $lineCount;
    }
    function main() {
    	$start = microtime(true);
    	$linesCount = countLinesInFile();
    	$duration = number_format(microtime(true)-$start, 4);
    	$memory = number_format(memory_get_peak_usage(true),0,'.',' ');
    	echo "Total lines: $linesCount; Duration: $duration seconds; Memory used: $memory bytes\n";
    }
    main();
    ?>

  • Python
    # coding: utf-8
    import time
    
    def linesCount(filename):
        count = 0
        with open(filename, 'r') as reader:
            while reader.readline():
                count += 1
    
        return count
    
    
    if __name__ == '__main__':
        filename = 'big.csv'
    
        start_time = time.time()
        count = linesCount(filename)
        end_time = time.time() - start_time
    
        print("Lines count: %s in %s" % (count, end_time))

  • Swift
    import Foundation
    import Darwin
    
    
    var path = "big.csv"
    
    let methodStart = NSDate()
    
    var linesCount = 0
    let bufsize = 32*1024
    var buf = UnsafeMutablePointer<Int8>.allocate(capacity: bufsize)
    
    let filePointer = fopen(path, "r")
    
    while fgets(buf, Int32(bufsize-1), filePointer) != nil {
        //    print(String.fromCString(CString(buf)))
        linesCount += 1
    }
    fclose(filePointer)
    
    let methodFinish = Date()
    let executionTime = methodFinish.timeIntervalSince(methodStart as Date)
    print("Execution time: \(executionTime)")
    
    print("Lines Count: \(linesCount)")


Тестовая среда


Все действия и тесты (кроме .Net) производились на MacBook Pro 13 Late 2012 с i5 2.5 GHz, SSD 256 Gb и 8 Gb ОЗУ, macOS 10.12.1.

Файл для тестов был найден в виде .csv размером 491,4 Мб и 489286 строк.

Для тестов были написаны простейшие консольные приложения состоящие из 2х функций: собственно, основной функции и функции, которая считает кол-во строк.

Результаты


C ? 0.25 сек
C# ? 2.8 сек
Go ? 0.16 сек
Java ? 2.7 сек
PHP ? 0.25 сек
Python ? 2.6 сек
Objective-C/Swift ? 0.25 сек

Оговорюсь, что я не делал большого количества итераций, остановившись на 20. В результаты взял среднее значение.

Из интересного


  • Первые тесты на C# (Mono) показали ужасающие 7.5 секунд, в то время как версия под Windows показала 3 секунды. Полагаю, ОС была чем-то занята, потому как на следующий день тесты выдали терпимые 2.5-3 сек.
  • Никак не ожидал, что будет что-то быстрее C. Go удивил
  • Не ожидал подобную скорость от PHP. Но C-шные вызовы дают о себе знать.
  • Objective-C/Swift благодаря возможности встроить C-код показали аналогичный самому C результат. Я смухлевал

P. S.


Исходные файлы на всякий случай прилеплены в Gist: исходные коды.

Если кто-то желает протестировать и поделиться результатами — милости прошу, мне крайне интересно.

И, повторюсь, интерес у меня крайне спортивный, стоит воздержаться от комментариев «Java — ***, а C — рулит!».

Удачи друзьям груммелей!
Поделиться с друзьями
-->

Комментарии (22)


  1. lair
    28.11.2016 19:01
    +3

    … вот только у вас в C#-ном коде постоянно создаются новые объекты строк, а в C/Go (остальные смотреть лень) — чтение в существующий буфер.


    1. Amka
      28.11.2016 19:04
      -3

      О, я пробовал использовать FileStream и .Read(). Увы, прироста в скорости я не получил ощутимого.


      1. lair
        28.11.2016 19:05

        И тем не менее результаты были бы более репрезентативными.


  1. AllexIn
    28.11.2016 19:02

    Полагаю, ОС была чем-то занята

    Кэшированием?


    1. Amka
      28.11.2016 19:04

      Не признаётся.


      1. AllexIn
        28.11.2016 19:10
        +5

        Стоит отметить, что реализация на С — делает лишнюю работу. А именно — копирование найденной строки. Что явно лишнее для вашей задачи.
        Реализация fgets сначала считывает блок данных, находит в нем окончание строки, после чего копирует найденную строку в буфер. Это, мягко говоря, излишнее для вашей задачи.
        В тоже время в Go вы никаких лишних копирований не делаете и просто считаете количество \n в буффере.(Насколько я понимаю. Go мне незнаком)
        Не удивительно, что Go в этом случае в выигрыше.


    1. KlimovDm
      28.11.2016 19:42

      Кэшированием?
      Похоже, что автор не учитывал существование page-cache


  1. biophreak
    28.11.2016 19:07
    +3

    Вообще говоря, очень странное сравнение. Абсолютно разных языков, один и тот же подход во всех, в некоторых вообще возможности фреймворков не используются, например.
    Почему в C# вы замеряли только одно решение с использованием стримридера?
    Почему не измерить что-то типа


    var lines = File.ReadAllLines("myfile").Length;

    Опять же, какие опции и версии компиляторов/интерпретаторов были использовании при сборке С примера? А JVM? А PHP? А в C# примере, с каким фреймворком собиралось? Какой конфиг?


    В общем — тест бестолковый и показывает ровным счетом ничего.


    В любом нормальном языке/технолигии, если разработчик не идиот, все упрется в любом случае в I/O.


    1. biophreak
      28.11.2016 19:19

      Стоит еще упомянуть в случае Java и C# — срабатывал ли GC, вы же создаете (вызовом ReadString) полмиллиона строк, но не используете их нигде, оно могло в процессе начать собираться и коллосально повлиять на время.


  1. TargetSan
    28.11.2016 19:08
    +11

    Во всех языках кроме Го вы вычитываете строки по одной. А в Го — заполняете буфер и считаете количество символов перевода строки.


    1. Nagg
      28.11.2016 19:20
      +8

      Зато Го быстрее Си!
      Забавляет когда люди не понимают что делают, делают выводы, постят это на популярных ресурсах, а потом к этому кто-то даже прислушивается. Посоны, го читает файлы быстрее чем си! А C# вообще час работает.


  1. lorc
    28.11.2016 19:11
    +4

    А
    # time wc -l big.csv
    пробовали?

    Вообще, я так понимаю, не было цели получить максимальную скорость? Я бы попробовал mmap() и тупой проход по памяти. Вряд ли можно будет сделать ещё эффективнее.


    1. AllexIn
      28.11.2016 19:19
      +2

      Ну и о чем тогда писать статью? :)
      «С» опять всех порвал на синтетическом тесте…
      А так Го победил, в тесте, где Го — единственный избежал некорректного кода.


  1. gbg
    28.11.2016 19:15
    +1

    Хорошо было бы на C сделать mmap файла в память (и залочить его там, сколько там верхний предел — 1500М? Детский размер, по нынешним меркам), а потом в мультитреде считать переводы строк.


    1. ForNeVeR
      28.11.2016 19:28
      +1

      Я предлагаю на всех языках тогда уж сделать mmap. И, как водится в таких бенчмарках, замерять перформанс непонятно чего :)


      1. gbg
        28.11.2016 19:57
        +1

        Если после этого где-то будет неодинаково, возникнет интересный вопрос — «а почему это вдруг простая беготня по памяти тормозит?»


  1. griganton
    28.11.2016 19:24

    А что если для питона сделать функцию попроще?

    def linesCount(filename):
        return sum(1 for line in open(filename))
    

    Кажется, должно работать чуток пошустрее.


    1. zodiak
      28.11.2016 19:51

      или так

      def linesCount(filename):
              with open(filename, 'rb') as fin:
                  return fin.read().count('\n')
      

      — избавление от построчного чтения (дополнительный поиск следующего разделителя) должно немного ускорить код


  1. YoungSkipper
    28.11.2016 19:30

    А дайте тестовый файл (в архиве он на гитхаб должен легко влезть)? Ибо на моих тестах банальный wc -l file.csv показывает результаты сравнимые с Go
    А если пошаманить предварительно побить файл через dd и обрабатывать части паралельно — то просто тупо упираемся в скорость диска


  1. MetaDone
    28.11.2016 19:44

    хотелось бы узнать результат у php-кода

    function countLinesInFile() {
        $file = new \SplFileObject('big.csv', 'r');
        $file->seek(PHP_INT_MAX);
        return $file->key() + 1;
    }
    


    1. MetaDone
      28.11.2016 19:50
      +1

      на локальном компе файл 1 миллион строк
      Total lines: 1000001; Duration: 0.1109 seconds; Memory used: 2 097 152 bytes — код что прислал выше
      Total lines: 1000000; Duration: 0.9270 seconds; Memory used: 2 097 152 bytes — код из статьи


  1. Dampir
    28.11.2016 20:14

    var startTime = DateTime.Now;
    var linesCount = countLinesInFile(filename);
    var spentTime = DateTime.Now - startTime;
    

    Так измерять время на C# не рекомендуется. Надо использовать Stopwatch (или хотя бы DateTime.UtcNow) Обсуждение на SO.