Страницы

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

четверг, 1 ноября 2018 г.

Как заставить .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 зависит только от второго цикла, что может повлечь оптимизации, которые повлияют на результаты тестов. После корректировки результат почти одинаковый - в пределах погрешности.


Ответ

Неужели такие простые методы не делаются 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 раза, что должно добавлять тормозов. Но на практике этот переход очень хорошо предсказывается современными процессорами. Так что практически цикл просто читает данные из массивов и пишет их в соответствующие байты результата.

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

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