【问题标题】:why is LZMA SDK (7-zip) so slow为什么 LZMA SDK (7-zip) 这么慢
【发布时间】:2012-08-30 19:57:12
【问题描述】:

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

2 秒编码

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

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

[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://stackoverflow.com/a/8775927/637142 的方法,它也需要很长时间。为什么比实际程序慢 10 倍?

还有

即使我设置为只使用一个线程:

仍然需要更少的时间(3 秒对 15 秒):


(编辑)另一种可能性

可能是因为 C# 比汇编或 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();
}

输出

注意 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");


    }

【问题讨论】:

  • 只是猜测 - 零售与调试有什么区别?
  • 我尝试在发布时破坏相同的东西,但没有区别:(
  • 如果您给应用一个预热期(例如运行 5 次迭代,然后进行分析),是否有任何改进。
  • 认为您为 c# 发布了错误的屏幕截图(我对结果很好奇)。
  • Cpp 44 秒,c# 153 秒。我希望 cpp 在某些方面会胜过 c#,但是这个练习相当于简单的带有原始类型的汇编指令(甚至 IL 代码也演示了这一点)。我怀疑 cpp 编译器是否正在优化循环,否则它会立即执行(也不能解释 7z 的性能差异)。我很想知道有什么区别;可能对调整 .Net 应用程序很有见地。

标签: c# performance compression 7zip lzma


【解决方案1】:

我对代码运行了分析器,最昂贵的操作似乎是搜索匹配项。在 C# 中,它一次搜索一个字节。 LzBinTree.cs 中有两个函数(GetMatches 和 Skip),其中包含以下代码 sn-p,它花费了大约 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%)。 Profiler 显示,以上内容将 BinTree.Skip 中的时间更改了 -45%,BinTree.GetMatches 更改了 +3%。
  • 那么对于相同的数据,托管代码与非托管代码相比如何?如果这不是您正在测试的数据的瓶颈,也许您可​​以找到其他可以改进的瓶颈。
【解决方案2】:

这种二进制算术和大量分支的代码是 C 编译器喜欢的,而 .NET JIT 讨厌的。 .NET JIT 不是一个非常聪明的编译器。它针对快速编译进行了优化。如果 Microsoft 想要调整它以获得最大性能,他们会插入 VC++ 后端,但后来故意不这样做。

另外,我可以通过您使用 7z.exe (6MB/s) 获得的速度判断您正在使用多个内核,可能使用 LZMA2。我的快速核心 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,在我看来还是很多:(
    • 是的,遗憾的是差异仍然很大。您是否检查过这两个文件(输出文件 - 即 .7z)是否相同(大小相同,二进制内容相同)?如果不是 - 请检查选项并确保您使用相同的方法(以及字典大小等)进行压缩,例如:“仅存储”或“最小压缩”非常快,但压缩得不是很好,而“最大压缩”则完全相反。
    • 是的,出于某种原因,用代码很难做到这一点。如果我将压缩设置为 Ultra(最高可能)并设置 1 个线程,则需要 6 秒。如果我然后将字大小更改为 786 MB,则需要 21 秒,所以也许这就是问题所在。我会尝试以某种方式在我的程序上设置字长并让你知道
    • 7-zip 的文档将默认设置显示为:dictionary - size [0, 29], default: 23 (8MB), umber of fast bytes - [5, 273], default: 128 等...我在 7zip 上设置了相同的比率,它快了 3 倍。我想这与程序有关......
    • 尝试测试两次操作。当 C# 生成的 MSIL 代码被执行时,是 JIT 编译的(这需要时间)。也可以从磁盘读取 dll 库。这两个操作都很漫长。对相同方法的后续调用不会导致从磁盘读取 dll 和 JIT 编译。
    【解决方案4】:

    我刚刚查看了 LZMA CS 实现,它都是在托管代码中执行的。最近针对我当前项目的压缩要求对此进行了一些调查,托管代码中的大多数压缩实现似乎都比本机代码效率低。

    我只能假设这是这里问题的原因。如果您查看另一个压缩工具 QuickLZ 的性能表,您会发现本机代码和托管代码(无论是 C# 还是 Java)之间的性能差异。

    我想到了两个选项:使用 .NET 的互操作工具来调用本机压缩方法,或者如果您可以牺牲压缩大小,请查看 http://www.quicklz.com/

    【讨论】:

      【解决方案5】:

      另一种选择是使用 SevenZipSharp(可在 NuGet 上获得)并将其指向您的 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 });
      

      【讨论】:

        【解决方案6】:

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

        这些是使 .net 运行时变慢的每条指令之前的隐式指令。在典型的业务应用程序中,我们并不关心性能,其中业务和 ui 逻辑的复杂性更为重要,这就是为什么 .net 运行时会格外小心地保护每条指令,以便我们快速调试和解决问题。

        原生 c 程序总是比 .net 运行时更快,但它们很难调试并且需要深入了解 c 才能编写正确的代码。因为 c 会执行一切,但不会给你任何异常或出错的线索。

        【讨论】:

          猜你喜欢
          • 2021-09-03
          • 2015-03-05
          • 2016-09-28
          • 2020-02-08
          • 2012-07-17
          • 2011-11-07
          • 2015-08-24
          • 2013-08-06
          • 2014-07-16
          相关资源
          最近更新 更多