Подтвердить что ты не робот

Улучшение линейных операций ввода-вывода в D

Мне нужно обрабатывать большое количество средних и больших файлов (от нескольких сотен МБ до ГБ) по-разному, поэтому меня интересуют стандартные подходы D для итерации по строкам. Идиома foreach(line; file.byLine()), похоже, соответствует счету и является приятным и легким и понятным, однако производительность кажется менее идеальной.

Например, ниже представлены три тривиальные программы в Python и D для итерации по строкам файла и подсчета строк. Для файла ~ 470 МБ (~ 3.6M строк) я получаю следующие тайминги (лучше всего из 10):

D раз:

real    0m19.146s
user    0m18.932s
sys     0m0.190s

Время Python (после EDIT 2, см. ниже):

real    0m0.924s
user    0m0.792s
sys     0m0.129s

Здесь версия D, скомпилированная с помощью dmd -O -release -inline -m64:

import std.stdio;
import std.string;

int main(string[] args)
{
  if (args.length < 2) {
    return 1;
  }
  auto infile = File(args[1]);
  uint linect = 0;
  foreach (line; infile.byLine())
    linect += 1;
  writeln("There are: ", linect, " lines.");
  return 0;
}

И теперь соответствующая версия Python:

import sys

if __name__ == "__main__":
    if (len(sys.argv) < 2):
        sys.exit()
    infile = open(sys.argv[1])
    linect = 0
    for line in infile:
        linect += 1
    print "There are %d lines" % linect

EDIT 2: я изменил код Python, чтобы использовать более идиоматический for line in infile, как это предлагается в комментариях ниже, что приводит к еще большему ускорению для версии Python, которая сейчас приближается скорость стандартного вызова wc -l для инструмента Unix wc.

Любые советы или указатели на то, что я могу сделать неправильно в D, что дает такую ​​плохую производительность?

EDIT. И для сравнения, здесь версия D, которая выкидывает идиому byLine() из окна и сразу же всасывает все данные в память, а затем разбивает данные на строки post-hoc, Это дает лучшую производительность, но по-прежнему примерно вдвое медленнее, чем у Python.

import std.stdio;
import std.string;
import std.file;

int main(string[] args)
{
  if (args.length < 2) {
    return 1;
  }
  auto c = cast(string) read(args[1]);
  auto l = splitLines(c);
  writeln("There are ", l.length, " lines.");
  return 0;
}

Тайминги для этой последней версии следующие:

real    0m3.201s
user    0m2.820s
sys     0m0.376s
4b9b3361

Ответ 1

EDIT AND TL; DR: эта проблема решена в https://github.com/D-Programming-Language/phobos/pull/3089. Улучшенная производительность File.byLine будет доступна с D 2.068.

Я попробовал свой код в текстовом файле с 575247 строками. Базовая линия Python занимает около 0.125 секунд. Здесь моя кодовая база с таймингами, встроенными в комментарии для каждого метода. Пояснения следуют.

import std.algorithm, std.file, std.stdio, std.string;

int main(string[] args)
{
  if (args.length < 2) {
    return 1;
  }
  size_t linect = 0;

  // 0.62 s
  foreach (line; File(args[1]).byLine())
    linect += 1;

  // 0.2 s
  //linect = args[1].readText.count!(c => c == '\n');

  // 0.095 s
  //linect = args[1].readText.representation.count!(c => c == '\n');

  // 0.11 s
  //linect = File(args[1]).byChunk(4096).joiner.count!(c => c == '\n');

  writeln("There are: ", linect, " lines.");
  return 0;
}

Я использовал dmd -O -release -inline для каждого варианта.

Первая версия (самая медленная) читает по одной строке за раз. Мы могли бы и должны улучшить производительность byLine; в настоящее время он усугубляется такими вещами, как смешанное использование byLine с другими операциями C stdio, что, вероятно, слишком консервативно. Если мы покончим с этим, мы сможем легко выполнить предварительную выборку и т.д.

Вторая версия считывает файл одним махом, а затем использует стандартный алгоритм для подсчета строк с предикатом.

В третьей версии признается тот факт, что нет необходимости учитывать любые тонкости UTF; подсчет байтов так же хорош, поэтому он преобразует строку в ее байтовое представление (бесплатно), а затем подсчитывает байты.

Последняя версия (мой fave) считывает 4KB данных из файла за раз и с лёгкостью сбрасывает их с помощью joiner. Затем он подсчитывает байты.

Ответ 2

Я думал, что сегодня буду делать что-то новое, поэтому решил "изучить" D. Обратите внимание, что это первый D, который я написал, поэтому я мог бы быть полностью отключен.

Первое, что я попробовал, - это буферизация вручную:

foreach (chunk; infile.byChunk(100000)) {
    linect += splitLines(cast(string) chunk).length;
}

Обратите внимание, что это неверно, поскольку оно игнорирует строки, пересекающие границы, но исправление, которое приходит позже.

Это помогло немного, но не достаточно. Это позволило мне проверить

foreach (chunk; infile.byChunk(100000)) {
    linect += (cast(string) chunk).length;
}

который показал, что все время находилось в splitLines.

Я сделал локальную копию splitLines. Только это увеличило скорость в 2 раза! Я этого не ожидал. Я бегу с обоими

dmd -release -inline -O -m64 -boundscheck=on
dmd -release -inline -O -m64 -boundscheck=off

Это примерно так же.

Затем я переписал splitLines, чтобы быть специализированным на s[i].sizeof == 1, который кажется только медленнее, чем Python, потому что он также разбивается на разделители абзацев.

Чтобы закончить работу, я сделал Range и оптимизировал его дальше, и этот код приближается к скорости Python. Учитывая, что Python не разбивается на разделители абзацев, а код, лежащий в его основе, написан на C, это кажется ОК. Этот код может иметь производительность O(n²) в строках длиной более 8k, но я не уверен.

import std.range;
import std.stdio;

auto lines(File file, KeepTerminator keepTerm = KeepTerminator.no) {
    struct Result {
        public File.ByChunk chunks;
        public KeepTerminator keepTerm;
        private string nextLine;
        private ubyte[] cache;

        this(File file, KeepTerminator keepTerm) {
            chunks = file.byChunk(8192);
            this.keepTerm = keepTerm;

            if (chunks.empty) {
                nextLine = null;
            }
            else {
                // Initialize cache and run an
                // iteration to set nextLine
                popFront;
            }
        }

        @property bool empty() {
            return nextLine is null;
        }

        @property auto ref front() {
            return nextLine;
        }

        void popFront() {
            size_t i;
            while (true) {
                // Iterate until we run out of cache
                // or we meet a potential end-of-line
                while (
                    i < cache.length &&
                    cache[i] != '\n' &&
                    cache[i] != 0xA8 &&
                    cache[i] != 0xA9
                ) {
                    ++i;
                }

                if (i == cache.length) {
                    // Can't extend; just give the rest
                    if (chunks.empty) {
                        nextLine = cache.length ? cast(string) cache : null;
                        cache = new ubyte[0];
                        return;
                    }

                    // Extend cache
                    cache ~= chunks.front;
                    chunks.popFront;
                    continue;
                }

                // Check for false-positives from the end-of-line heuristic
                if (cache[i] != '\n') {
                    if (i < 2 || cache[i - 2] != 0xE2 || cache[i - 1] != 0x80) {
                        continue;
                    }
                }

                break;
            }

            size_t iEnd = i + 1;
            if (keepTerm == KeepTerminator.no) {
                // E2 80 A9 or E2 80 A9
                if (cache[i] != '\n') {
                    iEnd -= 3;
                }
                // \r\n
                else if (i > 1 && cache[i - 1] == '\r') {
                    iEnd -= 2;
                }
                // \n
                else {
                    iEnd -= 1;
                }
            }

            nextLine = cast(string) cache[0 .. iEnd];
            cache = cache[i + 1 .. $];
        }
    }

    return Result(file, keepTerm);
}

int main(string[] args)
{
    if (args.length < 2) {
        return 1;
    }

    auto file = File(args[1]);
    writeln("There are: ", walkLength(lines(file)), " lines.");

    return 0;
}

Ответ 3

Это спорно, является ли подсчет строк является хорошим показателем общей производительности в приложениях обработки текста. Вы тестируете эффективность библиотеки python C, как и все остальное, и вы получите разные результаты, как только начнете делать полезные вещи с данными. У D было меньше времени, чем Python, чтобы отточить стандартную библиотеку, и задействовано меньше людей. Производительность byLine обсуждается уже пару лет, и я думаю, что следующий выпуск будет быстрее.

Люди действительно считают D эффективным и продуктивным для текстовой обработки именно такого рода. Например, AdRoll хорошо известен как магазин python, но их ребята из области данных используют D:

http://tech.adroll.com/blog/data/2014/11/17/d-is-for-data-science.html

Чтобы вернуться к вопросу, очевидно, что сравнивать компиляторы и библиотеку так же, как и один язык. Роль DMD - это ссылочный компилятор, который быстро компилируется. Таким образом, это отлично подходит для быстрой разработки и итерации, но если вам нужна скорость, вы должны использовать LDC или GDC, а если вы используете DMD, включите оптимизацию и отключите проверку границ.

На моей 64-битной машине HP Probook 4530s с аркой Linux, использующей последние 1 мм строки уэстбери-сервера WestburyLab, я получаю следующее:

python2: real 0m0.333s, пользователь 0m0.253s, sys 0m0.013s

pypy (разогретый): real 0m0.286s, пользователь 0m0.250s, sys 0m0.033s

DMD (по умолчанию):               real 0m0.468s, пользователь 0m0.460s, sys 0m0.007s

DMD (-O -release -inline -noboundscheck):               real 0m0.398s, пользователь 0m0.393s, sys 0m0.003s

GDC (по умолчанию): реальный 0m0.400s, пользователь 0m0.380s, sys 0m0.017s [Я не знаю переключателей для оптимизации GDC]

LDC (по умолчанию): real 0m0.396s, пользователь 0m0.380s, sys 0m0.013s

LDC (-O5): реальный 0m0.336s, пользователь 0m0.317s, sys 0m0.017s

В реальном приложении можно использовать встроенный профилировщик для определения горячих точек и настройки кода, но я согласен с тем, что наивный D должен быть приличной скоростью и, в худшем случае, в том же самом шаге, что и python. И использование LDC с оптимизацией, которая действительно то, что мы видим.

Для полноты я изменил ваш код D на следующее. (Некоторые из импорта не нужны - я играл).

import std.stdio;
import std.string;
import std.datetime;
import std.range, std.algorithm;
import std.array;

int main(string[] args)
{
  if (args.length < 2) {
    return 1;
  }
  auto t=Clock.currTime();
  auto infile = File(args[1]);
  uint linect = 0;
  foreach (line; infile.byLine)
    linect += 1;
  auto t2=Clock.currTime-t;
  writefln("There are: %s lines and took %s", linect, t2);
  return 1;
}

Ответ 4

Это будет быстрее, чем ваша версия, нежели версия python:

module main;

import std.stdio;
import std.file;
import std.array;

void main(string[] args)
{
    auto infile = File(args[1]);
    auto buffer = uninitializedArray!(char[])(100);
    uint linect;
    while(infile.readln(buffer))
    {
        linect += 1;
    }
    writeln("There are: ", linect, " lines.");
}

Ответ 5

Строки

tl; dr автоматически декодируются, что замедляет работу splitLines.

Текущая реализация splitLines декодирует строку "на лету", что делает ее медленной. В следующей версии phobos это будет fixed.

Будет диапазон, который сделает это и для вас.

В общем, D GC не соответствует уровню техники, однако D дает вам возможность производить меньше мусора. Чтобы получить конкурентоспособную программу, вам нужно избегать бесполезных распределений. Вторая важная вещь: для быстрого использования кода используйте gdc или ldc, потому что сила dmd заключается в быстром генерации кода и не быстрого кода.

Итак, я не успел, но эта версия не должна выделяться после самой большой строки, потому что она повторно использует буфер и не расшифровывает UTF.

import std.stdio;

void main(string[] args)
{
    auto f = File(args[1]);
    // explicit mention ubyte[], buffer will be reused
    // no UTF decoding, only looks for "\n". See docs.
    int lineCount;
    foreach(ubyte[] line; std.stdio.lines(f))
    {
        lineCount += 1;
    }

    writeln("lineCount: ", lineCount);
}

Версия, использующая диапазоны, может выглядеть так, если вам нужно что каждая строка заканчивается терминатором:

import std.stdio, std.algorithm;

void main(string[] args)
{
    auto f = File(args[1]);

    auto lineCount = f.byChunk(4096) // read file by chunks of page size 
`    .joiner // "concatenate" these chunks
     .count(cast(ubyte) '\n'); // count lines
    writeln("lineCount: ", lineCount);
}

В следующем выпуске просто сделайте, чтобы приблизиться к оптимальной производительности и взломать все пробелы, пробивающие строки.

void main(string[] args)
{
    auto f = File(args[1]);

    auto lineCount = f.byChunk(4096) // read file by chunks of page size 
     .joiner // "concatenate" these chunks
     .lineSplitter // split by line
     .walkLength; // count lines
    writeln("lineCount: ", lineCount);
}

Ответ 6

int main()
{
    import std.mmfile;
    scope mmf = new MmFile(args[1]);
    foreach(line; splitter(cast(string)mmf[], "\n"))
    {
        ++linect;
    }
    writeln("There are: ", linect, " lines.");
    return 0;
}