Почему LZMA SDK (7-zip) настолько медленный
Я нашел 7-zip отлично, и я хотел бы использовать его в приложениях .net. У меня есть файл размером 10 МБ (a.001), и он принимает:
![enter image description here]()
2 секунды для кодирования.
Теперь будет хорошо, если бы я мог сделать то же самое на С#. Я загрузил http://www.7-zip.org/sdk.html Исходный код LZMA SDK С#. Я в основном копировал каталог CS в консольное приложение в visual studio:
![enter image description here]()
Затем я скомпилировал и eveything скомпилировал плавно. Поэтому в выходной директории я разместил файл a.001
размером 10 МБ. В основном методе, который пришел на исходный код, я разместил:
[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 секунд! Я также пробовал метод qaru.site/info/67202/..., и это также занимает очень много времени. Почему он в 10 раз медленнее, чем реальная программа?
Также
Даже если я решил использовать только один поток:
![enter image description here]()
По-прежнему требуется гораздо меньше времени (3 секунды против 15):
(Изменить) Другая возможность
Может быть, потому, что С# медленнее сборки или 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");
}
Выход
![enter image description here]()
С#
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]()
Обратите внимание, насколько медленнее был С#. Обе программы, которые запускаются из-за пределов визуальной студии в режиме выпуска. Возможно, именно по этой причине это занимает намного больше времени в .net, чем на С++.
Также я получил те же результаты. С# был в 3 раза медленнее, как на примере, который я только что показал!
Заключение
Я не могу понять, что вызывает проблему. Думаю, я буду использовать 7z.dll и вызывать необходимые методы из С#. Библиотека, которая делает это: 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");
}
Ответы
Ответ 1
Этот тип двоично-арифметического и разветвляющегося кода - это то, что любят C-компиляторы и что ненавидит .NET JIT..NET JIT не очень умный компилятор. Он оптимизирован для быстрой компиляции. Если Microsoft захочет настроить его на максимальную производительность, они будут подключаться к серверу VС++, но затем намеренно этого не делают.
Кроме того, я могу сказать по скорости, которую вы получаете с 7z.exe(6 МБ/с), что вы используете несколько ядер, возможно, используя LZMA2. Мой быстрый ядро i7 может доставлять 2 Мбайт/с на ядро, поэтому я думаю, что 7z.exe работает многопоточно для вас. Попробуйте включить threading в 7zip-библиотеке, если это возможно.
Я рекомендую, чтобы вместо использования LZMA-алгоритма управляемого кода вы либо использовали изначально скомпилированную библиотеку, либо вызывали 7z.exe с помощью Process.Start
. Последний должен заставить вас начать очень быстро с хорошими результатами.
Ответ 2
Я запустил профайлер кода, и самая дорогая операция, похоже, находится в поиске совпадений. В С# он ищет один байт за раз. В 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))
{
И если вы используете небезопасный код и отбрасываете байт * на 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;
}
}
}
Ответ 3
Я сам не использовал LZMA SDK, но я уверен, что по умолчанию 7-zip запускает большую часть операций над многими потоками. Поскольку я не сделал этого сам, единственное, что я могу предложить, - проверить, можно ли заставить его использовать много потоков (если оно не используется по умолчанию).
Изменить:
Похоже, что потоки могут не быть (единственной) проблемой, связанной с производительностью, есть и другие, о которых я мог подумать:
-
Вы проверили, что вы установили те же самые параметры, что и при использовании 7-zip-интерфейса? Является ли выходной файл того же размера? Если нет - может случиться, что один метод сжатия намного быстрее, чем другой.
-
Выполняется ли ваше приложение с VS или нет? Если это так - это может также добавить некоторые накладные расходы (но я думаю, это не должно приводить к тому, что приложение работает в 5 раз медленнее).
- Есть ли какие-либо другие операции перед сжатием файла?
Ответ 4
Я только что рассмотрел реализацию LZMA CS, и все это выполнялось в управляемом коде. Недавно сделав некоторое расследование этого требования сжатия для моего текущего проекта, большинство реализаций сжатия в управляемом коде, похоже, работают менее эффективно, чем в native.
Я могу только предположить, что это является причиной проблемы здесь. Если вы посмотрите таблицу производительности другого инструмента сжатия QuickLZ, вы увидите разницу в производительности между собственным и управляемым кодом (будь то С# или 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 runtime медленнее, чем собственные инструкции. Если что-то пойдет не так в c, у нас обычно появляется крах приложения с синим экраном смерти. Но в С# это не так, потому что любые проверки, которые мы не делаем в c, фактически добавлены в С#. Не добавляя лишнюю проверку на нуль, среда выполнения никогда не сможет исключить исключение нулевого указателя. Не проверяя индекс и длину, среда выполнения никогда не может исключить исключение.
Это неявные инструкции перед каждой инструкцией, которая замедляет время выполнения .net. В типичных бизнес-приложениях мы не заботимся о производительности, где важна сложность бизнес-логики и ui-логики, поэтому время выполнения .net защищает каждую инструкцию с особой осторожностью, что позволяет нам быстро отлаживать и разрешать проблемы.
Нативные программы c всегда будут быстрее, чем время выполнения .net, но их трудно отлаживать и нужно углубленно знать c для написания правильного кода. Потому что c выполнит все, но не даст вам никаких исключений или подсказок о том, что пошло не так.