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)
AllexIn
28.11.2016 19:02Полагаю, ОС была чем-то занята
Кэшированием?Amka
28.11.2016 19:04Не признаётся.
AllexIn
28.11.2016 19:10+5Стоит отметить, что реализация на С — делает лишнюю работу. А именно — копирование найденной строки. Что явно лишнее для вашей задачи.
Реализация fgets сначала считывает блок данных, находит в нем окончание строки, после чего копирует найденную строку в буфер. Это, мягко говоря, излишнее для вашей задачи.
В тоже время в Go вы никаких лишних копирований не делаете и просто считаете количество \n в буффере.(Насколько я понимаю. Go мне незнаком)
Не удивительно, что Go в этом случае в выигрыше.
biophreak
28.11.2016 19:07+3Вообще говоря, очень странное сравнение. Абсолютно разных языков, один и тот же подход во всех, в некоторых вообще возможности фреймворков не используются, например.
Почему в C# вы замеряли только одно решение с использованием стримридера?
Почему не измерить что-то типа
var lines = File.ReadAllLines("myfile").Length;
Опять же, какие опции и версии компиляторов/интерпретаторов были использовании при сборке С примера? А JVM? А PHP? А в C# примере, с каким фреймворком собиралось? Какой конфиг?
В общем — тест бестолковый и показывает ровным счетом ничего.
В любом нормальном языке/технолигии, если разработчик не идиот, все упрется в любом случае в I/O.
biophreak
28.11.2016 19:19Стоит еще упомянуть в случае Java и C# — срабатывал ли GC, вы же создаете (вызовом ReadString) полмиллиона строк, но не используете их нигде, оно могло в процессе начать собираться и коллосально повлиять на время.
TargetSan
28.11.2016 19:08+11Во всех языках кроме Го вы вычитываете строки по одной. А в Го — заполняете буфер и считаете количество символов перевода строки.
Nagg
28.11.2016 19:20+8Зато Го быстрее Си!
Забавляет когда люди не понимают что делают, делают выводы, постят это на популярных ресурсах, а потом к этому кто-то даже прислушивается. Посоны, го читает файлы быстрее чем си! А C# вообще час работает.
lorc
28.11.2016 19:11+4А
# time wc -l big.csv
пробовали?
Вообще, я так понимаю, не было цели получить максимальную скорость? Я бы попробовал mmap() и тупой проход по памяти. Вряд ли можно будет сделать ещё эффективнее.AllexIn
28.11.2016 19:19+2Ну и о чем тогда писать статью? :)
«С» опять всех порвал на синтетическом тесте…
А так Го победил, в тесте, где Го — единственный избежал некорректного кода.
gbg
28.11.2016 19:15+1Хорошо было бы на C сделать mmap файла в память (и залочить его там, сколько там верхний предел — 1500М? Детский размер, по нынешним меркам), а потом в мультитреде считать переводы строк.
griganton
28.11.2016 19:24А что если для питона сделать функцию попроще?
def linesCount(filename): return sum(1 for line in open(filename))
Кажется, должно работать чуток пошустрее.zodiak
28.11.2016 19:51или так
def linesCount(filename): with open(filename, 'rb') as fin: return fin.read().count('\n')
— избавление от построчного чтения (дополнительный поиск следующего разделителя) должно немного ускорить код
YoungSkipper
28.11.2016 19:30А дайте тестовый файл (в архиве он на гитхаб должен легко влезть)? Ибо на моих тестах банальный wc -l file.csv показывает результаты сравнимые с Go
А если пошаманить предварительно побить файл через dd и обрабатывать части паралельно — то просто тупо упираемся в скорость диска
MetaDone
28.11.2016 19:44хотелось бы узнать результат у php-кода
function countLinesInFile() { $file = new \SplFileObject('big.csv', 'r'); $file->seek(PHP_INT_MAX); return $file->key() + 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 — код из статьи
lair
… вот только у вас в C#-ном коде постоянно создаются новые объекты строк, а в C/Go (остальные смотреть лень) — чтение в существующий буфер.
Amka
О, я пробовал использовать FileStream и .Read(). Увы, прироста в скорости я не получил ощутимого.
lair
И тем не менее результаты были бы более репрезентативными.