multithreading - Perl 脚本随着进度变慢

标签 multithreading performance perl memory-management

我编写了一个 Perl 脚本来编译显示,以便用户可以查看它们。有数千个这样的显示文件(DSET 文件)需要编译,这个过程需要相当长的时间(4-5 小时)。显示是使用外部可执行文件编译的(我没有关于此可执行文件内部工作的详细信息)。

作为加快进程的解决方案,我们决定并行运行此可执行文件的多个实例,以尝试大幅提高性能。

使用 16 个线程运行时,性能显着提高,现在需要大约 1 小时才能完成,而不是 4-5 小时,但仍然存在问题。随着脚本的进行,此可执行文件运行所需的时间会增加。

我对大约 1000 个 DSET 文件进行了测试,并随着 Perl 脚本的进行监视外部编译程序的执行时间。下面是执行时间随时间增加的图。

performance plot

如您所见,当脚本启动时,Perl 脚本需要大约 4 秒的时间来打开可执行文件、编译 DSET,然后关闭可执行文件。一旦脚本处理了大约 500 个 DSET,编译每个后续 DSET 所需的时间似乎会增加。当脚本接近尾声时,一些 DSET 文件需要长达 12 秒的时间来编译!

以下是每个线程执行的功能的示例:

# Build the displays
sub fgbuilder {
    my ($tmp_ddldir, $outdir, $eset_files, $image_files) = @_;

    # Get environment variables
    my $executable = $ENV{fgbuilder_executable};
    my $version    = $ENV{fgbuilder_version   };

    # Create the necessary directories
    my $tmp_imagedir = "$tmp_ddldir\\images";
    my $tmp_outdir   = "$tmp_ddldir\\compiled";
    make_path($tmp_ddldir, $tmp_imagedir, $tmp_outdir);

    # Copy the necessary files
    map { copy($_, $tmp_ddldir  ) } @{$eset_files };
    map { copy($_, $tmp_imagedir) } @{$image_files};

    # Take the next DSET off of the queue
    while (my $dset_file = $QUEUE->dequeue()) {

        # Copy the DSET to the thread's ddldir
        copy($dset_file, $tmp_ddldir);

        # Get the DSET name
        my $dset          = basename($dset_file);
        my $tmp_dset_file = "$tmp_ddldir\\$dset";

        # Build the displays in the DSET
        my $start = time;
        system $executable,
            '-compile' ,
            '-dset'    , $dset        ,
            '-ddldir'  , $tmp_ddldir  ,
            '-imagedir', $tmp_imagedir,
            '-outdir'  , $tmp_outdir  ,
            '-version' , $version     ;
        my $end = time;
        my $elapsed = $end - $start;

        $SEMAPHORE->down();
        open my $fh, '>>', "$ENV{fgbuilder_errordir}\\test.csv";
        print {$fh} "$PROGRESS,$elapsed\n";
        close $fh;
        $SEMAPHORE->up();

        # Remove the temporary DSET file
        unlink $tmp_dset_file;

        # Move all output files to the outdir
        recursive_move($tmp_outdir, $outdir);

        # Update the progress
        { lock $PROGRESS; $PROGRESS++; }
        my $percent = $PROGRESS/$QUEUE_SIZE*100;
        { local $| = 1; printf "\rBuilding displays ... %.2f%%", $percent; }
    }

    return;
}

每次通过循环时,它都会生成一个显示构建可执行文件的新实例,等待它完成,然后关闭该实例(这应该释放它正在使用的任何内存并解决我所看到的任何问题)。

这些线程中有 16 个并行运行,每个线程从队列中取出一个新的 DSET,编译它并将编译后的显示移动到输出目录。一旦显示被编译,它就会继续从队列中取出另一个 DSET 并重新启动该过程,直到队列用完为止。

几天来,我一直在挠头,试图弄清楚为什么它会变慢。在此过程中,我的 RAM 使用率稳定且没有增加,而且我的 CPU 使用率还没有达到最大值。对这里可能发生的事情的任何帮助或见解表示赞赏。

编辑

我编写了一个测试脚本来尝试测试问题是由磁盘 I/O 缓存问题引起的理论。在这个脚本中,我采用了与旧脚本相同的基本主体,并用我自己的任务替换了对可执行文件的调用。

这是我将可执行文件替换为:
    # Convert the file to hex (multiple times so it takes longer! :D)
    my @hex_lines = ();
    open my $ascii_fh, '<', $tmp_dset_file;
    while (my $line = <$ascii_fh>) {
        my $hex_line = unpack 'H*', $line;
        $hex_line = unpack 'H*', $hex_line;
        $hex_line = unpack 'H*', $hex_line;
        $hex_line = unpack 'H*', $hex_line;
        $hex_line = unpack 'H*', $hex_line;
        $hex_line = unpack 'H*', $hex_line;
        $hex_line = unpack 'H*', $hex_line;
        $hex_line = unpack 'H*', $hex_line;
        push @hex_lines, $hex_line;
    }
    close $ascii_fh;

    # Print to output files
    make_path($tmp_outdir);
    open my $hex_fh, '>', "$tmp_outdir\\$dset" or die "Failed to open file: $!";
    print {$hex_fh} @hex_lines;
    close $hex_fh;
    open $hex_fh, '>', "$tmp_outdir\\2$dset" or die "Failed to open file: $!";
    print {$hex_fh} @hex_lines;
    close $hex_fh;
    open $hex_fh, '>', "$tmp_outdir\\3$dset" or die "Failed to open file: $!";
    print {$hex_fh} @hex_lines;
    close $hex_fh;
    open $hex_fh, '>', "$tmp_outdir\\4$dset" or die "Failed to open file: $!";
    print {$hex_fh} @hex_lines;
    close $hex_fh;
    open $hex_fh, '>', "$tmp_outdir\\5$dset" or die "Failed to open file: $!";
    print {$hex_fh} @hex_lines;
    close $hex_fh;
    open $hex_fh, '>', "$tmp_outdir\\6$dset" or die "Failed to open file: $!";
    print {$hex_fh} @hex_lines;
    close $hex_fh;

我没有调用可执行文件并编译 DSET,而是将每个文件作为文本文件打开,而是进行一些简单的处理并将一些文件写入磁盘(我每次都将一些文件写入磁盘,因为可执行文件将多个文件写入它处理的每个 DSET 的磁盘)。然后我监控处理时间并绘制我的结果。

这是我的结果:

Processing time vs script progression

我确实相信我的另一个脚本的部分问题是磁盘 I/O 问题,但正如您在此处看到的,由于我故意创建的磁盘 I/O 问题,处理时间的增加不是逐渐的。它有一个突然的跳跃,然后结果变得相当不可预测。

在我之前的脚本中,您可以看到一些不可预测性,并且它正在写入大量文件,因此我毫不怀疑该问题至少部分是由磁盘 I/O 问题引起的,但这仍然无法解释为什么处理时间的增加是渐进的,并且似乎以恒定的速度增加。

我相信这里还有一些我们没有考虑的其他因素在起作用。

最佳答案

我认为你只是有磁盘碎片问题。鉴于您有多个线程不断创建和删除不同大小的新文件,最终磁盘空间变得非常碎片化。我不知道你在哪个操作系统下运行它,我猜它是 Windows。

您无法使用测试工具重现此问题的原因可能是由于您的外部编译器工具的行为 - 它可能会创建输出文件,然后在写入之间以不同的时间多次扩展其大小,这往往会创建重叠的文件它们在多线程中运行时的磁盘空间,尤其是在磁盘使用率相对较高的情况下,例如超过 70%。您测试似乎正在序列化文件创建,从而避免并发写入碎片。

可能的解决方案:

  • 对磁盘驱动器进行碎片整理。只需将编译的文件复制到另一个分区/磁盘,删除它们并复制回来就足够了。
  • 在几个不同的独立分区上运行您的外部编译器以避免碎片化。
  • 确保您的文件系统有 50% 或更多的可用空间。
  • 使用不太容易出现文件系统碎片的操作系统,例如Linux。
  • 关于multithreading - Perl 脚本随着进度变慢,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/29970853/

    相关文章:

    sql-server - SQL内存-查看实际使用的内存和可用内存

    perl - 散列[关联数组?]上的Perl语法错误

    multithreading - 使用带有线程的 Net::ssh::perl 模块时出现段错误

    c# - 将单线程应用程序迁移到多线程、并行执行、蒙特卡洛模拟

    swift - DispatchQueue.main.async 和 Dispatch.main.asyncAfter(.now(),{}) 之间的区别

    c++ - 在基于范围的 for 循环中使用转发引用有什么好处?

    c++ - : executing a Perl script from C++ via system call, 和调用 DLL 文件哪个性能更好?

    multithreading - 如何在 Windows 服务中使用 Threadpool.QueueUserWorkItem?

    C# 从调用线程外部的静态类访问函数

    angular - 如何测试现代 Angular 应用程序的性能/负载