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