Поставили задачу написать алгоритм сложения элементов массива на asm(ассемблерные вставки на с),c#,c и сравнить время исполнения. Написал простенький код для сложения int-ов.
double __declspec(dllexport) asm_time(int* array, int N) {
int* p = array;
int sum = 0;
_asm
{
mov eax, p
xor ebx, ebx
mov ecx, 0
cycle:
add ebx, [eax + 4 * ecx]
inc ecx
cmp ecx, N
jne cycle
mov sum, ebx
}
return 1;
}
Код вроде бы работает, но при N > 100000000 вылетает исключение: [
Так же, попытался переписать код под float, которые, как я понял, имеют одинаковый размер с int. Программа так-же падает на порядках 5-6 степени десятки, но в отличие от int, выдает странный результат типа:-4.65661e-10. Так-же есть небольшой вопрос по поводу производительности. Может ли быть такое? Уж сильно получается asm шустрый, или нужно искать ошибку в алгоритме? Заранее спасибо за ответы
upd:
кнопка с генерацией массива:
private void button1_Click(object sender, EventArgs e)
{
Random rnd = new Random();
int N;
Int32.TryParse(textBox1.Text, out N);
int[] array = new int[N];
for (int i = 0; i < N; i++)
{
array[i] = rnd.Next(0, 10);
}
textBox3.Text = Convert.ToString(CObj.getTime(array, N));
textBox4.Text = Convert.ToString(AsmObj.getTime(array, N));
textBox2.Text = Convert.ToString(ShapObj.getTime(array, N));
}
Передача массива в функцию:
class testTimeAsm
{
[DllImport(@"C:\Users\евгений\source\repos\AsmLibrary\Debug\AsmLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern double asm_time(int [] array, int N);
public double getTime(int [] array, int N)
{
var startTime = System.Diagnostics.Stopwatch.StartNew();
asm_time(array, N);
startTime.Stop();
var resultTime = startTime.Elapsed;
return resultTime.TotalMilliseconds;
}
}
К сожалению, я давно слез с x86, но написал небольшой пример, как можно прикруть ассемблер к C#. Дело том, что в x64 все просто с вызовом функций, там только одна конвенция вызова - fastcall. Она описана подробнейшим образом здесь.
При этом так как я пишу на ассемблере под x64, то С++ с его примочкой __asm
для меня закрыт, потому что Visual C++ не поддерживает ассемблерные вставки в x64.
Самое очевидное для меня - использовать FASM.
Далее прямо в редакторе FASMW пишу вот такую либу. И собираю в меню Run->Compile.
CalcArraySum
- для int[]
CalcArraySumD
- для double[]
format PE64 console DLL
entry DllEntryPoint
include 'win64a.inc'
section '.text' code readable executable
proc DllEntryPoint hinstDLL,fdwReason,lpvReserved
mov eax,TRUE
ret
endp
proc CalcArraySum uses rbx rcx rdx, pArray,dwLength
mov rbx,rcx
xor rax,rax
xor rcx,rcx
next:
add eax,[rbx + 4 * rcx]
inc rcx
cmp rcx,rdx
jne next
ret
endp
proc CalcArraySumD uses rbx rcx rdx, pArray,dwLength
mov rbx,rcx
pxor xmm0,xmm0
xor rcx,rcx
dnext:
addsd xmm0,[rbx + 8 * rcx]
inc rcx
cmp rcx,rdx
jne dnext
ret
endp
section '.edata' export data readable
export 'Math64.dll',\
CalcArraySum,'CalcArraySum',\
CalcArraySumD,'CalcArraySumD'
section '.reloc' fixups data readable discardable
if $=$$
dd 0,8 ; if there are no fixups, generate dummy entry
end if
Получаю Math64.dll на выходе.
Копирую библиотеку в папку bin/Debug/net5.0 своего консольного проекта, кстати вот он:
class Program
{
private static readonly Random rnd = new Random();
private static readonly int vectorSize = Vector<int>.Count;
static void Main(string[] args)
{
int n = 100000000;
int[] array = Generate(n);
Stopwatch sw = new Stopwatch();
sw.Start();
int sum = LoopSum(array);
sw.Stop();
Console.WriteLine("LoopSum");
Console.WriteLine("Result: {0} Elapsed: {1}ms", sum, sw.ElapsedMilliseconds);
sw.Restart();
sum = VectorSum(array);
sw.Stop();
Console.WriteLine("VectorSum");
Console.WriteLine("Result: {0} Elapsed: {1}ms", sum, sw.ElapsedMilliseconds);
sw.Restart();
sum = AsmSum(array);
sw.Stop();
Console.WriteLine("AsmSum");
Console.WriteLine("Result: {0} Elapsed: {1}ms", sum, sw.ElapsedMilliseconds);
double[] arrayD = GenerateD(n);
sw.Restart();
double sumD = LoopSumD(arrayD);
sw.Stop();
Console.WriteLine("LoopSumD");
Console.WriteLine("Result: {0} Elapsed: {1}ms", sumD, sw.ElapsedMilliseconds);
sw.Restart();
sumD = AsmSumD(arrayD);
sw.Stop();
Console.WriteLine("AsmSumD");
Console.WriteLine("Result: {0} Elapsed: {1}ms", sumD, sw.ElapsedMilliseconds);
Console.ReadKey();
}
private static int[] Generate(int length)
=> Enumerable.Range(default, length).Select(x => rnd.Next(0, 10)).ToArray();
private static int LoopSum(int[] array)
{
int result = 0;
for (int i = 0; i < array.Length; i++)
result += array[i];
return result;
}
private static int VectorSum(int[] array)
{
Vector<int> accVector = Vector<int>.Zero;
int i;
for (i = 0; i <= array.Length - vectorSize; i += vectorSize)
accVector = Vector.Add(accVector, new Vector<int>(array, i));
return array[i..].Aggregate(Vector.Dot(accVector, Vector<int>.One), (x, y) => x + y);
}
[DllImport("Math64.dll")]
private static extern int CalcArraySum(int[] array, int length);
private static int AsmSum(int[] array)
=> CalcArraySum(array, array.Length);
private static double[] GenerateD(int length)
=> Enumerable.Range(default, length).Select(x => rnd.NextDouble()).ToArray();
private static double LoopSumD(double[] array)
{
double result = 0;
for (int i = 0; i < array.Length; i++)
result += array[i];
return result;
}
[DllImport("Math64.dll")]
private static extern double CalcArraySumD(double[] array, int length);
private static double AsmSumD(double[] array)
=> CalcArraySumD(array, array.Length);
}
И получаю вот такой вывод в консоль
LoopSum
Result: 449976668 Elapsed: 281ms
VectorSum
Result: 449976668 Elapsed: 112ms
AsmSum
Result: 449976668 Elapsed: 52ms
LoopSumD
Result: 50000545,02830217 Elapsed: 299ms
AsmSumD
Result: 50000545,02830217 Elapsed: 107ms
В качестве развлечения попробовал написать векторный SIMD вариант с использованием System.Numerics.Vector
. Проект .NET 5 x64, но должен без проблем отработать и в .NET Core 3.1.
P.S. Вот еще одна полезная ссылка про x64 ассемблер, PDF документ на сайте Intel, там тоже упоминается конвенция вызовов, возможно даже более вменяемо описано, чем у Microsoft.
Спасибо @AlexanderPetrov. Вот вывод с релизного билда.
LoopSum
Result: 449999618 Elapsed: 79ms
VectorSum
Result: 449999618 Elapsed: 30ms
AsmSum
Result: 449999618 Elapsed: 50ms
LoopSumD
Result: 50004374,29094455 Elapsed: 108ms
AsmSumD
Result: 50004374,29094455 Elapsed: 106ms
Про ebx сентенцию убрал - и на ответ уже вроде не тянет, но пусть повисит пока.
Сложение вещественных чисел осуществляется совсем другими инструкциями (fadd
на стеке сопроцессора x87 или addss
для SSE), поэтому не стоит удивляться результату
Кстати, векторное сложение c padd (int) или addps (float)
будет втрое быстрее, да и умный компилятор должен это сам делать, если уровень оптимизации соответствующий выставлен.
Горизонтальное сложение из коммента - 4 суммы накоплены в xmm1:
phaddd xmm1, xmm1
phaddd xmm1, xmm1
movd sum, xmm1
Ещё такой момент - для большого размера массива наверняка случится переполнение, результат будет неверен (аналогично и для небольшого массива с крупными числами)
Задание: Необходимо разработать программу, в которой было реализовано два потока (нити)Эти потоки должны запускаться одновременно и сортировать...
Если честно, даже предположений нет в чём ошибкаВозникает после запуска в одном из двух мест, в зависимости от того, какое из чисел больше