Страницы

Поиск по вопросам

четверг, 19 декабря 2019 г.

Как заставить .NET оптимизировать код?

#c_sharp #net


Сравнивая производительность 

public static int GetR(int argb)
{
    return (argb >> 16) & 0xff;
}
...
for (int i = 0; i < max; i++)
{
    temp1 = GetR(i);
}


и 

for (int i = 0; i < max; i++)
{
    temp2 = (i >> 16) & 0xff;
}


Обнаружил, что первый способ работает чуть медленнее. По дизассемблированному коду
studio видно, что метод не подставляется inline.
Неужели такие простые методы не делаются inline?
.Net 4.5 (интересует также для .Net 4.0 и желательно для 3.5, 2.0), компилирую в
стандартной конфигурации Release VS2012.  

Аналогично:

public static int BuildArgb(int alpha, int red, int green, int blue)
{
    return (red << 16) | (green << 8) | (blue << 0) | (alpha << 24);
}

... 
for (int i = 0; i < sz; i++)
{
    color = BuildArgb(aBytes[i], rBytes[i], gBytes[i], bBytes[i]);
}


и 

for (int i = 0; i < sz; i++)
{
    color = (rBytes[i] << 16) | (gBytes[i] << 8) | (bBytes[i]) | (aBytes[i] << 24);
}


Можно ли попросить компилятор оптимизировать код ?

UPD
Код измерений для второго примера:

private static void Test3(int sz)
{
    Console.WriteLine("Test BuildArgb");

    Random random = new Random(255);
    byte[] aBytes = new byte[sz];
    byte[] rBytes = new byte[sz];
    byte[] gBytes = new byte[sz];
    byte[] bBytes = new byte[sz];
    for (int i = 0; i < sz; i++)
    {
        aBytes[i] = (byte)random.Next();
        rBytes[i] = (byte)random.Next();
        gBytes[i] = (byte)random.Next();
        bBytes[i] = (byte)random.Next();
    }

    int color = 0;

    var s2 = Stopwatch.StartNew();
    for (int i = 0; i < sz; i++)
    {
        color = BuildArgb(aBytes[i], rBytes[i], gBytes[i], bBytes[i]);
    }

    s2.Stop();

    var s3 = Stopwatch.StartNew();
    for (int i = 0; i < sz; i++)
    {
        color = (rBytes[i] << 16) | (gBytes[i] << 8) | (bBytes[i] << 0) | (aBytes[i]
<< 24);
    }

    s3.Stop();

    Console.WriteLine("BuildArgb: " + s2.Elapsed.TotalMilliseconds);
    Console.WriteLine("Inline: " + s3.Elapsed.TotalMilliseconds);
    Console.WriteLine(color);
}


UOD 2 

Полный код примера 2

namespace ConsoleApplication1
{
    using System;

    using System.Diagnostics;

    internal class Program
    {
        public static int BuildArgb(int alpha, int red, int green, int blue)
        {
            return (red << 16) | (green << 8) | (blue << 0) | (alpha << 24);
        }

        private static void Main()
        {
            const int max = int.MaxValue;

            //Test1(max);
            //Test2(1024 * 1024 * 100);
            Test3(1024 * 1024 * 100);

            Console.Read();
        }           

        private static void Test3(int sz)
        {
            Console.WriteLine("Test BuildArgb");

            Random random = new Random(255);
            byte[] aBytes = new byte[sz];
            byte[] rBytes = new byte[sz];
            byte[] gBytes = new byte[sz];
            byte[] bBytes = new byte[sz];
            for (int i = 0; i < sz; i++)
            {
                aBytes[i] = (byte)random.Next();
                rBytes[i] = (byte)random.Next();
                gBytes[i] = (byte)random.Next();
                bBytes[i] = (byte)random.Next();
            }

            int color = 0;

            var s2 = Stopwatch.StartNew();
            for (int i = 0; i < sz; i++)
            {
                color = BuildArgb(aBytes[i], rBytes[i], gBytes[i], bBytes[i]);
            }

            s2.Stop();

            var s3 = Stopwatch.StartNew();
            for (int i = 0; i < sz; i++)
            {
                color = (rBytes[i] << 16) | (gBytes[i] << 8) | (bBytes[i] << 0) |
(aBytes[i] << 24);
            }

            s3.Stop();

            Console.WriteLine("BuildArgb: " + s2.Elapsed.TotalMilliseconds);
            Console.WriteLine("Inline: " + s3.Elapsed.TotalMilliseconds);
            Console.WriteLine(color);  // Использование переменной, что бы циклы
не скомпилировались пустыми
        }
    }
}


UPD 3
Тестовый код нужно подкорректировать, результат переменной color зависит только от
второго цикла, что может повлечь оптимизации, которые повлияют на результаты тестов.
После корректировки результат почти одинаковый - в пределах погрешности.
    


Ответы

Ответ 1



Неужели такие простые методы не делаются inline? Во втором примере JIT вполне успешно инлайнит метод при запуске в конфигурации Release не под отладкой. Более того, полученный нативный код практически совпадает для первого и второго вариантов, отличается лишь порядок применения сдвигов. Это можно легко проверить: Дописать Debugger.Launch(); где-то в теле Test3 Выбрать конфигурацию Release Запустить не под отладчиком В появившемся окне выбрать уже открытых экземпляр студии Кликнуть правой кнопкой рядом с местом остановки, выбрать Go To Disassembly Для второго случая результат примерно такой: for (int i = 0; i < sz; i++) { temp1 = GetR(i); } превратилось в for (int i = 0; i < sz; i++) 00E6055A xor eax,eax for (int i = 0; i < sz; i++) 00E6055C test ebx,ebx 00E6055E jle 00E60570 00E60560 mov ecx,eax 00E60562 sar ecx,10h 00E60565 and ecx,0FFh 00E6056B inc eax 00E6056C cmp eax,ebx 00E6056E jl 00E60560 } а второй цикл for (int i = 0; i < sz; i++) { temp2 = (i >> 16) & 0xff; } превратился в for (int i = 0; i < sz; i++) 00E60570 xor eax,eax for (int i = 0; i < sz; i++) 00E60572 test ebx,ebx 00E60574 jle 00E60586 { temp2 = (i >> 16) & 0xff; 00E60576 mov edx,eax 00E60578 sar edx,10h 00E6057B and edx,0FFh for (int i = 0; i < sz; i++) 00E60581 inc eax 00E60582 cmp eax,ebx 00E60584 jl 00E60576 } Как видно, результат полностью одинаковый - так что любая разница - это просто погрешность измерений. Само интересное, что если temp1 и temp2 не используются после из расчета, то JIT просто выбрасывает сам расчет: for (int i = 0; i < sz; i++) 0062054E xor eax,eax for (int i = 0; i < sz; i++) 00620550 test ebx,ebx 00620552 jle 00620559 00620554 inc eax 00620555 cmp eax,ebx 00620557 jl 00620554 } Вы же используете temp1 и temp2 после тела цикла? Если нет - то вы просто измеряете скорость перемотки цикла. Для второго примера картина та же - ассемблерный код практически совпадает, отличается лишь порядок применения сдвигов: for (int i = 0; i < sz; i++) xor edx,edx test ebx,ebx jle 00ED063E mov eax,dword ptr [ebp-28h] { color = Argb32Helper.MakeArgb(aBytes[i], rBytes[i], gBytes[i], bBytes[i]); mov eax,dword ptr [ebp-28h] cmp edx,dword ptr [eax+4] jae 00ED077F movzx eax,byte ptr [eax+edx+8] mov dword ptr [ebp-24h],eax mov eax,dword ptr [ebp-2Ch] cmp edx,dword ptr [eax+4] jae 00ED077F movzx esi,byte ptr [eax+edx+8] mov eax,dword ptr [ebp-30h] cmp edx,dword ptr [eax+4] jae 00ED077F movzx ecx,byte ptr [eax+edx+8] mov eax,dword ptr [ebp-34h] cmp edx,dword ptr [eax+4] jae 00ED077F movzx edi,byte ptr [eax+edx+8] { color = Argb32Helper.MakeArgb(aBytes[i], rBytes[i], gBytes[i], bBytes[i]); mov eax,dword ptr [ebp-24h] shl eax,10h shl esi,8 or eax,esi or eax,ecx shl edi,18h or eax,edi mov dword ptr [ebp-10h],eax for (int i = 0; i < sz; i++) inc edx cmp edx,ebx jl 00ED05DD } call нет, метод заинлайнен. для второго цикла: for (int i = 0; i < sz; i++) xor ecx,ecx test ebx,ebx jle 00ED06EB mov eax,dword ptr [ebp-2Ch] { color = (rBytes[i] << 16) | (gBytes[i] << 8) | (bBytes[i] << 0) | (aBytes[i] << 24); mov eax,dword ptr [ebp-2Ch] cmp ecx,dword ptr [eax+4] jae 00ED077F movzx eax,byte ptr [eax+ecx+8] shl eax,10h mov edx,dword ptr [ebp-30h] cmp ecx,dword ptr [edx+4] jae 00ED077F movzx edx,byte ptr [edx+ecx+8] shl edx,8 or eax,edx mov edx,dword ptr [ebp-34h] cmp ecx,dword ptr [edx+4] jae 00ED077F movzx edx,byte ptr [edx+ecx+8] or eax,edx mov edx,dword ptr [ebp-28h] cmp ecx,dword ptr [edx+4] jae 00ED077F movzx edx,byte ptr [edx+ecx+8] shl edx,18h or eax,edx mov dword ptr [ebp-10h],eax for (int i = 0; i < sz; i++) inc ecx cmp ecx,ebx jl 00ED0690 } Разница в выполнении чуть заметна, и зависит от порядка вызова циклов - тот, что вызывается первым, всегда немного медленее - именно поэтому в тестах всегда надо делать разогрев. Бонус: для совсем нового JIT из 4.6 на моей машине код совпадает еще больше - тело цикла будет состоит из 4-х блоков вида mov eax,dword ptr [rbx+8] cmp ecx,eax jae 00007FF805D207F8 movsxd rax,ecx movzx eax,byte ptr [rbx+rax+10h] и отличается только порядком блоков. первая проверка - mov/cmp/jae - это проверка выхода за границы массива. Формально она выполняется в цикле 4 раза, что должно добавлять тормозов. Но на практике этот переход очень хорошо предсказывается современными процессорами. Так что практически цикл просто читает данные из массивов и пишет их в соответствующие байты результата.

Ответ 2



Заставить не получится. Если вы хотите указать компилятору чтобы он по возможности заинлайнл вызов метода, используйте атрибут [MethodImpl(MethodImplOptions.AggressiveInlining)] Работает с версии 4.5. Вот тут более подробно: Aggressive Inlining in the CLR 4.5 JIT. UPD. По дизассемблированному коду studio видно, что метод не подставляется inline. Проверять это рефлектором бесполезно. Данная оптимизация происходит на этапе JIT, который преобразует промежуточный язык .NET в машинный код.

Комментариев нет:

Отправить комментарий