сложение элементов массива через ассемблерные вставки

304
16 мая 2022, 16:20

Поставили задачу написать алгоритм сложения элементов массива на 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;

    }
}
Answer 1

К сожалению, я давно слез с 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
Answer 2

Про ebx сентенцию убрал - и на ответ уже вроде не тянет, но пусть повисит пока.

Сложение вещественных чисел осуществляется совсем другими инструкциями (fadd на стеке сопроцессора x87 или addss для SSE), поэтому не стоит удивляться результату

Кстати, векторное сложение c padd (int) или addps (float) будет втрое быстрее, да и умный компилятор должен это сам делать, если уровень оптимизации соответствующий выставлен.

Горизонтальное сложение из коммента - 4 суммы накоплены в xmm1:

phaddd xmm1, xmm1
phaddd xmm1, xmm1
movd sum, xmm1

Ещё такой момент - для большого размера массива наверняка случится переполнение, результат будет неверен (аналогично и для небольшого массива с крупными числами)

READ ALSO
Помогите с регуляркой на C#!

Помогите с регуляркой на C#!

Есть регулярка, вот она:

182
Не могу синхронизировать потоки c#

Не могу синхронизировать потоки c#

Задание: Необходимо разработать программу, в которой было реализовано два потока (нити)Эти потоки должны запускаться одновременно и сортировать...

151
Index was outside the bounds of the array

Index was outside the bounds of the array

Если честно, даже предположений нет в чём ошибкаВозникает после запуска в одном из двух мест, в зависимости от того, какое из чисел больше

313