【问题标题】:Improving line-wise I/O operations in D改进 D 中的逐行 I/O 操作
【发布时间】:2015-05-09 10:44:15
【问题描述】:

我需要以逐行方式处理大量中型到大型文件(几百 MB 到 GB),因此我对用于迭代行的标准 D 方法感兴趣。 foreach(line; file.byLine()) 成语似乎符合要求,简洁易读,但性能似乎不太理想。

例如,下面是 Python 和 D 中的两个简单程序,用于迭代文件的行并计算行数。对于约 470 MB 文件(约 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

编辑 2:我更改了 Python 代码以使用更惯用的 for line in infile,如下面的 cmets 中所建议的那样,从而为 Python 版本带来了更大的加速,现在即将推出标准 wc -l 调用 Unix wc 工具的速度。

关于我在 D 中可能做错的任何建议或指示,导致性能如此糟糕?

编辑:为了比较,这里有一个 D 版本,它将 byLine() 成语抛出窗口并立即将所有数据吸入内存,然后将数据分成几行 post-hoc .这提供了更好的性能,但仍然比 Python 版本慢约 2 倍。

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

【问题讨论】:

  • 尝试了不同版本的 dmd (2.067.0-b3, 2.066.1, 2.064.2),结果大致相同。罪魁祸首似乎是-m64。在本地,对于由短行(不超过 100 个字符)组成的 200M 文件,32 位版本的运行速度比 Python 快一点(1.5 对 1.8 秒),但 64 位版本需要 6.9 秒,这比 32 位差 4 倍以上。也许某种 64 位代码生成效率低下,值得在 issues.dlang.org 上报告为错误。
  • 附带说明,另一个优化标志是“-noboundscheck”(或自 2.066 起支持的替代形式“-boundscheck=off”)。它完全禁用数组边界检查。也就是说,在这种情况下它并没有多大帮助。
  • 当我在没有“-m64”标志的情况下进行编译时,性能会稍差(尽管我使用的是 64 位机器,OS X 10.10;dmd v2.066)
  • 使用-m32 标志失败并出现ld: symbol(s) not found for architecture i386 错误。我已经在 dlang.org 网站上打开了一个问题,包括指向我用于测试目的的文件的链接。见issues.dlang.org/show_bug.cgi?id=14256。感谢您的帮助。
  • readlines 将所有内容读入内存; list(file) 是一种更惯用的方法,但在这种情况下,您应该只使用 for line in infile。请注意,如果您只想比较纯 IO 速度,则应考虑使用更快的可迭代计数方法 like given here - CPython 不是快速解释器。

标签: python io d


【解决方案1】:

编辑和 TL;DR:这个问题已在 https://github.com/D-Programming-Language/phobos/pull/3089 中解决。改进的File.byLine 性能将从 D 2.068 开始提供。

我在一个包含 575247 行的文本文件上尝试了您的代码。 Python 基线大约需要 0.125 秒。这是我的代码库,每种方法的 cmets 中嵌入了计时。解释如下。

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 的微妙之处。计算字节数也一样好,因此它将字符串转换为按字节表示的表示(免费),然后计算字节数。

最后一个版本(我的最爱)一次从文件中读取 4KB 的数据,并使用 joiner 懒惰地将它们展平。然后它再次计算字节数。

【讨论】:

  • Andrei 的回答对 D 中的 IO 提供了一些见解,但我同意它并没有真正解决我一直在努力解决的关键问题——如何以逐行方式有效地遍历文件。在任何实际应用程序中,我都会处理行/提取信息等。行计数示例主要是为了说明 D 中逐行迭代的缓慢行为。
  • @Veedrac:嗯,你是对的 - 陷入手头的微基准测试中。我只是看了一下东西,男孩可以改进代码。见github.com/D-Programming-Language/phobos/pull/3089。在相同的测试条件下,byLine 版本现在需要 0.136 秒。
【解决方案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 编写的,这似乎没问题。此代码可能在超过 8k 的行上具有 O(n²) 性能,但我不确定。

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;
}

【讨论】:

  • 这是最快的 D 代码示例,实际上可以让我对这些行进行一些处理。上面示例输入文件的时间是:real 0m1.339s user 0m1.190s sys 0m0.144s
【解决方案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,则打开优化并关闭边界检查。

在我的 arch linux 64 位 HP Probook 4530s 机器上,使用 WestburyLab usenet 语料库的最后 1 毫米行,我得到以下信息:

python2:真实0m0.333s,用户0m0.253s,系统0m0.013s

pypy(预热):real 0m0.286s,user 0m0.250s,sys 0m0.033s

DMD(默认): 真实0m0.468s,用户0m0.460s,系统0m0.007s

DMD(-O -release -inline -n​​oboundscheck): 真实0m0.398s,用户0m0.393s,系统0m0.003s

GDC(默认):real 0m0.400s,user 0m0.380s,sys 0m0.017s [我不知道 GDC 优化的开关]

LDC(默认):real 0m0.396s,user 0m0.380s,sys 0m0.013s

LDC(-O5): real 0m0.336s, user 0m0.317s, sys 0m0.017s

在实际应用程序中,人们将使用内置分析器来识别热点并调整代码,但我同意 naive 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;
}

【讨论】:

  • 我无法发表评论,但下面 Kozzi11 的示例在我的机器上确实更快,使用 dmd 优化在 0.255 秒时进入。可能是本地机器问题。您正在运行哪个版本的 DMD?任何其他信息都会有所帮助。
【解决方案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.");
}

【讨论】:

  • 其实我在本地测试时和-m64有同样的问题。此外,无论是 32 位还是 64 位,它仍然比具有较长行数的 Python 慢。我将添加一些测试生成器和结果到issues.dlang.org/show_bug.cgi?id=14256
【解决方案5】:

tl;dr 字符串是自动解码的,这会使 splitLines 变慢。

splitLines 的当前实现动态解码字符串,这使得它变慢了。在下一版本的 phobos 中,这将是 fixed

range 也会为您执行此操作。

一般来说,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);
}

【讨论】:

  • 请解释否决票。这个答案对我来说看起来不错,所以否决票让我有点困惑。还要考虑到潘克实际上是新人,所以不解释就投反对票是特别有害的。
  • 我对您的第一个示例充满希望,因为它有助于逐行处理,但不幸的是,时间安排是我尝试过的示例中最差的。在同一个数据集上,我测试了我得到的原始代码:real 1m1.199s user 1m0.213s sys 0m0.618s
【解决方案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;
}

【讨论】:

  • 简单地发布代码不会有帮助,请解释它的作用。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2015-02-13
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多