为什么LZMA SDK(7-zip)如此缓慢

24

我发现7-zip很好用,想在.NET应用程序中使用它。 我有一个10MB的文件(a.001),它需要:

enter image description here

2秒钟进行编码

如果我能在C#上做同样的事情就太好了。我已经下载了http://www.7-zip.org/sdk.html LZMA SDK c#源代码。我基本上将CS目录复制到Visual Studio的控制台应用程序中: enter image description here

然后我编译了一切都顺利。在输出目录中,我放置了大小为10MB的文件a.001。在源代码的主方法中,我放置了:

[STAThread]
    static int Main(string[] args)
    {
        // e stands for encode
        args = "e a.001 output.7z".Split(' '); // added this line for debug

        try
        {
            return Main2(args);
        }
        catch (Exception e)
        {
            Console.WriteLine("{0} Caught exception #1.", e);
            // throw e;
            return 1;
        }
    }
当我执行控制台应用程序时,应用程序工作得很好,并且我在工作目录中获得了输出 a.7z 。 问题在于它需要很长时间。 它需要大约15秒才能执行!我还尝试了 https://dev59.com/FXVC5IYBdhLWcg3wpjB8#8775927 的方法,但它也需要很长时间。为什么它比实际程序慢10倍?
此外,
即使我设置仅使用一个线程: enter image description here 它仍然需要较少的时间(3秒 vs 15):
(编辑)另一种可能性
这是因为C#比汇编语言或C语言慢吗? 我注意到算法执行了许多重操作,例如比较以下两个代码块。 它们都做同样的事情:
#include <time.h>
#include<stdio.h>

void main()
{
    time_t now; 

    int i,j,k,x;
    long counter ;

    counter = 0;

    now = time(NULL);

    /* LOOP  */
    for(x=0; x<10; x++)
    {
        counter = -1234567890 + x+2;

        for (j = 0; j < 10000; j++)     
            for(i = 0; i< 1000; i++)                
                for(k =0; k<1000; k++)
                {
                    if(counter > 10000)
                        counter = counter - 9999;
                    else
                        counter= counter +1;
                }

        printf (" %d  \n", time(NULL) - now); // display elapsed time
    }


    printf("counter = %d\n\n",counter); // display result of counter        

    printf ("Elapsed time = %d seconds ", time(NULL) - now);
    gets("Wait");
}

输出

在此输入图像描述

c#

static void Main(string[] args)
{       
    DateTime now;

    int i, j, k, x;
    long counter;

    counter = 0;

    now = DateTime.Now;

    /* LOOP  */
    for (x = 0; x < 10; x++)
    {
        counter = -1234567890 + x + 2;

        for (j = 0; j < 10000; j++)            
            for (i = 0; i < 1000; i++)                
                for (k = 0; k < 1000; k++)
                {
                    if (counter > 10000)
                        counter = counter - 9999;
                    else
                        counter = counter + 1;
                }


        Console.WriteLine((DateTime.Now - now).Seconds.ToString());            
    }

    Console.Write("counter = {0} \n", counter.ToString());
    Console.Write("Elapsed time = {0} seconds", DateTime.Now - now);
    Console.Read();
}

输出

enter image description here

请注意c#比C++慢了多少。这两个程序都是在Visual Studio外部的发布模式下运行的。也许这就是为什么在.net中需要比c ++更长时间的原因。

我得到了相同的结果。 就像我刚刚展示的例子一样,C#慢了3倍!


结论

我似乎不知道问题出在哪里。 我想我将使用7z.dll并从c#中调用必要的方法。 一个可以实现这一功能的库在:http://sevenzipsharp.codeplex.com/ ,这样我就可以使用与7zip相同的库。

    // dont forget to add reference to SevenZipSharp located on the link I provided
    static void Main(string[] args)
    {
        // load the dll
        SevenZip.SevenZipCompressor.SetLibraryPath(@"C:\Program Files (x86)\7-Zip\7z.dll");

        SevenZip.SevenZipCompressor compress = new SevenZip.SevenZipCompressor();

        compress.CompressDirectory("MyFolderToArchive", "output.7z");


    }

2
如果给应用程序一个预热期(例如运行5次迭代,然后进行分析),那么是否会有任何改进? - Tim M.
1
你可能上传了错误的 C# 截图(我对结果很感兴趣)。 - Tim M.
是的,这两个项目都在以下链接中:https://dl.dropbox.com/u/81397375/Demo.zip。请以发布模式编译这两个项目,并在 Visual Studio 之外运行它们。让我知道你的结果! - Tono Nam
1
Cpp 44秒,c# 153秒。我预计Cpp在某些方面会胜过C#,但这个练习只涉及基本类型的简单汇编指令(即使IL代码也是如此)。我怀疑Cpp编译器没有优化循环,否则它将立即执行(这也无法解释7z性能差异)。我很想知道差异在哪里;这可能对调整.Net应用程序有所帮助。 - Tim M.
尝试使用RyuJIT CTP4进行第二个示例,您会发现它与CPP在相同的时间内运行。 - Onur Gumus
显示剩余7条评论
6个回答

11

我对代码进行了分析,发现最耗费时间的操作是搜索匹配项。在C#中,它是逐字节搜索的。在LzBinTree.cs中有两个函数(GetMatches和Skip),其中包含以下代码片段,大约有40-60%的时间花在这段代码上:

if (_bufferBase[pby1 + len] == _bufferBase[cur + len])
{
    while (++len != lenLimit)
        if (_bufferBase[pby1 + len] != _bufferBase[cur + len])
            break;

这基本上是逐个字节查找匹配长度。我将其提取到自己的方法中:

if (GetMatchLength(lenLimit, cur, pby1, ref len))
{

如果您使用的是不安全代码,并将byte*转换为ulong*,每次比较8个字节而不是1个字节,那么在我的测试数据中速度几乎翻了一倍(在64位进程中):

private bool GetMatchLength(UInt32 lenLimit, UInt32 cur, UInt32 pby1, ref UInt32 len)
{
    if (_bufferBase[pby1 + len] != _bufferBase[cur + len])
        return false;
    len++;

    // This method works with or without the following line, but with it,
    // it runs much much faster:
    GetMatchLengthUnsafe(lenLimit, cur, pby1, ref len);

    while (len != lenLimit
        && _bufferBase[pby1 + len] == _bufferBase[cur + len])
    {
        len++;
    }
    return true;
}

private unsafe void GetMatchLengthUnsafe(UInt32 lenLimit, UInt32 cur, UInt32 pby1, ref UInt32 len)
{
    const int size = sizeof(ulong);
    if (lenLimit < size)
        return;
    lenLimit -= size - 1;
    fixed (byte* p1 = &_bufferBase[cur])
    fixed (byte* p2 = &_bufferBase[pby1])
    {
        while (len < lenLimit)
        {
            if (*((ulong*)(p1 + len)) == *((ulong*)(p2 + len)))
            {
                len += size;
            }
            else
                return;
        }
    }
}

在我的示例(x64)工作负载(161 MB)中,总体压缩时间从142.19秒减少到了140.29秒(提升了1.3%)。分析器显示以上更改使得BinTree.Skip的时间降低了45%,而BinTree.GetMatches的时间增加了3%。 - Joseph Kingry
那么托管代码与非托管代码在相同数据方面有何区别?如果这不是您正在测试的数据的瓶颈,也许您可以找到其他可以改进的瓶颈。 - Bryce Wagner

9
这种二进制算术和分支-heavy的代码是C编译器喜欢的,但是.NET JIT却不喜欢。.NET JIT并不是一个非常聪明的编译器,它被优化用于快速编译。如果微软想要将其调整为最大性能,他们将会插入VC++后端,但故意不这样做。
另外,我可以通过7z.exe(6MB/s)的速度告诉你,你正在使用多个核心,可能正在使用LZMA2。我的快速core i7每个核心可以提供2MB/s的速度,所以我猜测7z.exe正在为你运行多线程。如果可能的话,请尝试在7zip库中打开线程。
我建议你不要使用托管代码LZMA算法,而是使用本地编译库或使用Process.Start调用7z.exe。后者应该能够让你快速开始并获得良好的结果。

3

我自己没有使用过LZMA SDK,但我非常确定,默认情况下7-zip在许多线程上运行大部分操作。因为我自己没有做过,我唯一可以建议的是检查是否可能强制它使用多个线程(如果默认情况下没有使用)。

编辑:


由于看起来线程可能不是(唯一的)性能相关问题,我还想到了其他问题:

  1. 您是否检查了设置与使用7-zip UI时相同的选项?输出文件大小是否相同?如果不是-可能会发生一个压缩方法比另一个更快的情况。

  2. 您是否从VS中执行应用程序?如果是-这也可能会增加一些开销(但我猜这不应该导致应用程序运行5倍慢)。

  3. 在压缩文件之前是否还有其他操作?

+1 谢谢!我在发布模式下构建了它,然后在 Visual Studio 之外执行,时间缩短到了 5 秒钟!但是仍然存在很大的差异。例如,在 C# 中压缩一个 40MB 的文件需要 21 秒钟,而在真正的程序中只需要 6 秒钟。我认为比例是 1:3,这在我看来仍然很多 :( - Tono Nam
是的,不幸的是差异仍然很大。您是否检查了两个文件(输出文件 - 即 .7z 文件)是否相同(大小相同,二进制内容相同)?如果没有,请检查选项,并确保您正在使用相同的方法进行压缩(以及字典大小等)。例如:“仅存储”或“最小压缩”非常快,但不能很好地压缩,“最大压缩”则相反。 - Maciek Talaska
是的,不知为何用代码实现这个很难。如果我将压缩设置为最高级别(Ultra),并设置 1 线程,则需要 6 秒钟。如果将字大小设置为 786 MB,则需要 21 秒钟,因此可能是问题所在。我将尝试在程序中设置字大小,并告知您结果。 - Tono Nam
7-zip的文档显示默认值为:字典大小[0,29],默认值:23(8MB),快速字节数量[5,273],默认值:128等等...我在7zip上设置了相同的比例,它运行速度提高了3倍。我猜这与程序有关... - Tono Nam
1
尝试对操作进行两次测试。当执行C#生成的MSIL代码时,它会被JIT编译(这需要时间)。此外,dll库可能会从磁盘中读取。这两个操作都很耗时。对同一方法的后续调用将不会导致从磁盘中读取dll或进行JIT编译。 - Artemix

3
我刚刚查看了LZMA CS实现,发现它全部都是在托管代码中执行。最近为了当前项目的压缩需求调查了一些托管代码中的压缩实现,大多数实现似乎比本地代码效率低。我只能推测这就是问题所在。如果您查看另一个压缩工具QuickLZ的性能表格,您可以看到本地代码和托管代码(无论是C#还是Java)之间的性能差异。
有两个选择:使用.NET的Interop功能调用本地压缩方法,或者如果您可以承受牺牲压缩大小,则可以查看http://www.quicklz.com/

3

另一个选择是使用NuGet上可用的SevenZipSharp并将其指向您的7z.dll。然后您的速度应该大致相同:

var libPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "7-zip", "7z.dll");
SevenZip.SevenZipCompressor.SetLibraryPath(libPath);
SevenZip.SevenZipCompressor compressor = new SevenZipCompressor();
compressor.CompressFiles(compressedFile, new string[] { sourceFile });

2

.net运行时比本地指令慢。如果在c中出现问题,通常会导致蓝屏应用程序崩溃。但在c#中不会,因为我们在c中没有进行的任何检查实际上都添加在了c#中。如果没有额外的空值检查,运行时就永远无法捕获空指针异常。如果没有检查索引和长度,运行时就永远无法捕获越界异常。

这些是每个使.net运行时变慢的指令之前的隐式指令。在典型的业务应用程序中,我们不关心性能,业务和UI逻辑的复杂性更为重要,因此.net运行时会对每个指令进行额外的保护,以便让我们快速调试和解决问题。

本地c程序将始终比.net运行时更快,但它们很难调试,并且需要深入了解c才能编写正确的代码。因为c将执行所有内容,但不会提供任何异常或提示出错信息。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接