Общая и не общая производительность в С#
Я написал два эквивалентных метода:
static bool F<T>(T a, T b) where T : class
{
return a == b;
}
static bool F2(A a, A b)
{
return a == b;
}
Разница во времени:
00: 00: 00.0380022
00: 00: 00,0170009
Код для тестирования:
var a = new A();
for (int i = 0; i < 100000000; i++)
F<A>(a, a);
Console.WriteLine(DateTime.Now - dt);
dt = DateTime.Now;
for (int i = 0; i < 100000000; i++)
F2(a, a);
Console.WriteLine(DateTime.Now - dt);
Кто-нибудь знает, почему?
В комментарии ниже dtb * показать CIL:
IL for F2: ldarg.0, ldarg.1, ceq, ret. IL for F<T>: ldarg.0, box !!T, ldarg.1, box !!T, ceq, ret.
Я думаю, что это ответ на мой вопрос, но какую магию я могу использовать, чтобы отрицать бокс?
Далее я использую код из Psilon:
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ConsoleApplication58
{
internal class Program
{
private class A
{
}
private static bool F<T>(T a, T b) where T : class
{
return a == b;
}
private static bool F2(A a, A b)
{
return a == b;
}
private static void Main()
{
const int rounds = 100, n = 10000000;
var a = new A();
var fList = new List<TimeSpan>();
var f2List = new List<TimeSpan>();
for (int i = 0; i < rounds; i++)
{
// Test generic
GCClear();
bool res;
var sw = new Stopwatch();
sw.Start();
for (int j = 0; j < n; j++)
{
res = F(a, a);
}
sw.Stop();
fList.Add(sw.Elapsed);
// Test not-generic
GCClear();
bool res2;
var sw2 = new Stopwatch();
sw2.Start();
for (int j = 0; j < n; j++)
{
res2 = F2(a, a);
}
sw2.Stop();
f2List.Add(sw2.Elapsed);
}
double f1AverageTicks = fList.Average(ts => ts.Ticks);
Console.WriteLine("Elapsed for F = {0} \t ticks = {1}", fList.Average(ts => ts.TotalMilliseconds),
f1AverageTicks);
double f2AverageTicks = f2List.Average(ts => ts.Ticks);
Console.WriteLine("Elapsed for F2 = {0} \t ticks = {1}", f2List.Average(ts => ts.TotalMilliseconds),
f2AverageTicks);
Console.WriteLine("Not-generic method is {0} times faster, or on {1}%", f1AverageTicks/f2AverageTicks,
(f1AverageTicks/f2AverageTicks - 1)*100);
Console.ReadKey();
}
private static void GCClear()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
}
Windows 7,.NET 4.5, Visual Studio 2012, выпуск, оптимизированный, без прикрепления.
64
Elapsed for F = 23.68157 ticks = 236815.7
Elapsed for F2 = 1.701638 ticks = 17016.38
Not-generic method is 13.916925926666 times faster, or on 1291.6925926666%
x 86
Elapsed for F = 6.713223 ticks = 67132.23
Elapsed for F2 = 6.729897 ticks = 67298.97
Not-generic method is 0.997522398931217 times faster, or on -0.247760106878314%
И у меня новая магия: x64 в три раза быстрее...
PS: Моя целевая платформа - x64.
Ответы
Ответ 1
Я сделал некоторые изменения в вашем коде, чтобы правильно измерить perf.
- Использовать секундомер
- Выполнить режим выпуска
- Предотвращение вложения.
- Используйте GetHashCode() для выполнения реальной работы.
- Посмотрите на сгенерированный код сборки
Вот код:
class A
{
}
[MethodImpl(MethodImplOptions.NoInlining)]
static bool F<T>(T a, T b) where T : class
{
return a.GetHashCode() == b.GetHashCode();
}
[MethodImpl(MethodImplOptions.NoInlining)]
static bool F2(A a, A b)
{
return a.GetHashCode() == b.GetHashCode();
}
static int Main(string[] args)
{
const int Runs = 100 * 1000 * 1000;
var a = new A();
bool lret = F<A>(a, a);
var sw = Stopwatch.StartNew();
for (int i = 0; i < Runs; i++)
{
F<A>(a, a);
}
sw.Stop();
Console.WriteLine("Generic: {0:F2}s", sw.Elapsed.TotalSeconds);
lret = F2(a, a);
sw = Stopwatch.StartNew();
for (int i = 0; i < Runs; i++)
{
F2(a, a);
}
sw.Stop();
Console.WriteLine("Non Generic: {0:F2}s", sw.Elapsed.TotalSeconds);
return lret ? 1 : 0;
}
Во время моих тестов не общая версия была немного быстрее (.NET 4.5 x32 Windows 7).
Но практически нет измеримой разницы в скорости. Я бы сказал, что они равны.
Для полноты здесь приведен код сборки общей версии:
Я получил код сборки через отладчик в режиме деблокирования с включенной оптимизацией JIT. По умолчанию это отключить оптимизацию JIT во время отладки, чтобы упростить установку контрольных точек и переменных.
Generic
static bool F<T>(T a, T b) where T : class
{
return a.GetHashCode() == b.GetHashCode();
}
push ebp
mov ebp,esp
push ebx
sub esp,8 // reserve stack for two locals
mov dword ptr [ebp-8],ecx // store first arg on stack
mov dword ptr [ebp-0Ch],edx // store second arg on stack
mov ecx,dword ptr [ebp-8] // get first arg from stack --> stupid!
mov eax,dword ptr [ecx] // load MT pointer from a instance
mov eax,dword ptr [eax+28h] // Locate method table start
call dword ptr [eax+8] //GetHashCode // call GetHashCode function pointer which is the second method starting from the method table
mov ebx,eax // store result in ebx
mov ecx,dword ptr [ebp-0Ch] // get second arg
mov eax,dword ptr [ecx] // call method as usual ...
mov eax,dword ptr [eax+28h]
call dword ptr [eax+8] //GetHashCode
cmp ebx,eax
sete al
movzx eax,al
lea esp,[ebp-4]
pop ebx
pop ebp
ret 4
Не общий
static bool F2(A a, A b)
{
return a.GetHashCode() == b.GetHashCode();
}
push ebp
mov ebp,esp
push esi
push ebx
mov esi,edx
mov eax,dword ptr [ecx]
mov eax,dword ptr [eax+28h]
call dword ptr [eax+8] //GetHashCode
mov ebx,eax
mov ecx,esi
mov eax,dword ptr [ecx]
mov eax,dword ptr [eax+28h]
call dword ptr [eax+8] //GetHashCode
cmp ebx,eax
sete al
movzx eax,al
pop ebx
pop esi
pop ebp
ret
Как вы можете видеть, общая версия выглядит несколько более неэффективной из-за большего количества операций с памятью в виде надписей, которые не идеальны, но на самом деле разница не измерима, поскольку все вписывается в кеш L1 процессора, что делает операции памяти менее дорогостоящими по сравнению с чистыми регистрационными операциями не общего варианта. Я подозреваю, что не универсальная версия должна немного улучшиться в реальном мире, если вам нужно заплатить за реальный доступ к памяти, не выходя из какого-либо кэша процессора.
Для всех практических целей оба этих метода идентичны. Вы должны посмотреть на другое место для повышения производительности в реальном мире. Сначала я рассмотрю шаблоны доступа к данным и используемые структуры данных. Алгоритмические изменения, как правило, приносят гораздо больший прирост, чем такой низкий уровень.
Edit1: Если вы хотите использовать ==, то вы найдете
00000000 push ebp
00000001 mov ebp,esp
00000003 cmp ecx,edx // Check for reference equality
00000005 sete al
00000008 movzx eax,al
0000000b pop ebp
0000000c ret 4
оба метода создают точно такой же машинный код. Любая разница, которую вы измеряли, - это ваши ошибки измерения.
Ответ 2
Ваш метод тестирования испорчен. Есть несколько больших проблем с тем, как вы это сделали.
Сначала вы не указали "warm-up". В .NET при первом доступе к нему он будет медленнее последующих вызовов, чтобы он мог загружать любые необходимые сборки. Если вы собираетесь выполнять такие тесты, вы должны выполнять каждую функцию хотя бы один раз, или первый тест для запуска будет иметь большой штраф. Идите вперед и поменяйте порядок, вы, скорее всего, увидите противоположные результаты.
Второй DateTime
работает только с точностью до 16 мс, поэтому при сравнении двух раз у вас есть +/- ошибка 32 ms. Разница между этими двумя результатами составляет 21 мкс, что также лежит в пределах экспериментальной ошибки. Вы должны использовать более точный таймер, например класс Stopwatch.
Наконец, не делайте искусственных тестов, подобных этому. Они не показывают вам никакой полезной информации, кроме прав на похвалы для одного класса. Вместо этого научитесь использовать Code Profiler. Это покажет вам, что замедляет ваш код, и вы можете принимать обоснованные решения о том, как решить проблему, а не "угадывать", что не использовать шаблонный класс сделает ваш код быстрее.
Вот пример кода, который показывает, как это должно быть сделано:
using System;
using System.Diagnostics;
namespace Sandbox_Console
{
class A
{
}
internal static class Program
{
static bool F<T>(T a, T b) where T : class
{
return a == b;
}
static bool F2(A a, A b)
{
return a == b;
}
private static void Main()
{
var a = new A();
Stopwatch st = new Stopwatch();
Console.WriteLine("warmup");
st.Start();
for (int i = 0; i < 100000000; i++)
F<A>(a, a);
Console.WriteLine(st.Elapsed);
st.Restart();
for (int i = 0; i < 100000000; i++)
F2(a, a);
Console.WriteLine(st.Elapsed);
Console.WriteLine("real");
st.Restart();
for (int i = 0; i < 100000000; i++)
F<A>(a, a);
Console.WriteLine(st.Elapsed);
st.Restart();
for (int i = 0; i < 100000000; i++)
F2(a, a);
Console.WriteLine(st.Elapsed);
Console.WriteLine("Done");
Console.ReadLine();
}
}
}
И вот результаты:
warmup
00:00:00.0297904
00:00:00.0298949
real
00:00:00.0296838
00:00:00.0297823
Done
При замене порядка двух последних всегда короче, так что они эффективны в то же время, что и в пределах экспериментальной ошибки.
Ответ 3
Прекратите беспокоиться о сроках, беспокоиться о правильности.
Эти методы эквивалентны не. Один из них использует class A
operator==
, а другой использует object
operator==
.
Ответ 4
Две вещи:
- Вы сравниваете с помощью
DateTime.Now
. Вместо этого используйте Stopwatch
.
- Вы используете код, который не находится в нормальных условиях. JIT, скорее всего, влияет на первый запуск, делая ваш первый метод медленнее.
Если вы измените порядок своих тестов (т.е. сначала протестируйте не общий метод), будет ли ваш результат обратным? Я бы так заподозрил. Когда я подключил ваш код к LINQPad, а затем скопировал его так, чтобы он выполнял оба теста дважды, время выполнения для второй итерации находилось в пределах несколько сотен клещей друг от друга.
Итак, в ответ на ваш вопрос: да, кто-то знает почему. Это потому, что ваш тест неточен!
Ответ 5
Я переписал ваш тестовый код:
var stopwatch = new Stopwatch();
var a = new A();
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < 100000000; i++)
F<A>(a, a);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
stopwatch.Reset();
stopwatch.Start();
for (int i = 0; i < 100000000; i++)
F2(a, a);
stopwatch.Stop();
Console.WriteLine(stopwatch.ElapsedMilliseconds);
Подмена порядка ничего не меняет.
CIL для общего метода:
L_0000: nop
L_0001: ldarg.0
L_0002: box !!T
L_0007: ldarg.1
L_0008: box !!T
L_000d: ceq
L_000f: stloc.0
L_0010: br.s L_0012
L_0012: ldloc.0
L_0013: ret
И для не-generic:
L_0000: nop
L_0001: ldarg.0
L_0002: ldarg.1
L_0003: ceq
L_0005: stloc.0
L_0006: br.s L_0008
L_0008: ldloc.0
L_0009: ret
Итак, операция бокса - причина вашей разницы во времени. Вопрос в том, почему добавлена операция бокса. Проверьте это, вопрос
Бокс при использовании дженериков в С#
Ответ 6
Я провел анализ производительности в профессиональном качестве несколько раз в своей карьере и имел пару наблюдений.
- Во-первых, тест слишком короткий, чтобы быть действительным. Мое правило состоит в том, что тест производительности должен работать в течение 30 минут или около того.
- Во-вторых, важно запустить тест много раз, чтобы получить диапазон таймингов.
- В-третьих, я удивлен, что компилятор не оптимизировал петли, поскольку результаты функции не используются, а вызываемые функции не имеют побочных эффектов.
- В-четвертых, микро-тесты часто вводят в заблуждение.
Я когда-то работал над командой компилятора, у которой была большая смелая производительность. Одна сборка ввела оптимизацию, которая устранила несколько инструкций для определенной последовательности. Он должен иметь улучшенную производительность, но вместо этого производительность одного теста резко снизилась. Мы работали на аппаратных средствах с прямым кэшем. Оказывается, что код цикла и функция, вызываемые во внутреннем цикле, занимали одну и ту же строку кэша с новой оптимизацией на месте, но не с предыдущим сгенерированным кодом. Другими словами, этот тест был действительно эталоном памяти и полностью зависел от хитов и промахов кэша памяти, тогда как авторы полагали, что они написали вычислительный тест.
Ответ 7
Кажется более справедливым, нет?: D
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
namespace ConsoleApplication58
{
internal class Program
{
private class A
{
}
private static bool F<T>(T a, T b) where T : class
{
return a == b;
}
private static bool F2(A a, A b)
{
return a == b;
}
private static void Main()
{
const int rounds = 100, n = 10000000;
var a = new A();
var fList = new List<TimeSpan>();
var f2List = new List<TimeSpan>();
for (int i = 0; i < rounds; i++)
{
//test generic
GCClear();
bool res;
var sw = new Stopwatch();
sw.Start();
for (int j = 0; j < n; j++)
{
res = F(a, a);
}
sw.Stop();
fList.Add(sw.Elapsed);
//test not-generic
GCClear();
bool res2;
var sw2 = new Stopwatch();
sw2.Start();
for (int j = 0; j < n; j++)
{
res2 = F2(a, a);
}
sw2.Stop();
f2List.Add(sw2.Elapsed);
}
double f1AverageTicks = fList.Average(ts => ts.Ticks);
Console.WriteLine("Elapsed for F = {0} \t ticks = {1}", fList.Average(ts => ts.TotalMilliseconds),
f1AverageTicks);
double f2AverageTicks = f2List.Average(ts => ts.Ticks);
Console.WriteLine("Elapsed for F2 = {0} \t ticks = {1}", f2List.Average(ts => ts.TotalMilliseconds),
f2AverageTicks);
Console.WriteLine("Not-generic method is {0} times faster, or on {1}%", f1AverageTicks/f2AverageTicks,
(f1AverageTicks/f2AverageTicks - 1)*100);
Console.ReadKey();
}
private static void GCClear()
{
GC.Collect();
GC.WaitForPendingFinalizers();
GC.Collect();
}
}
}
На моем ноутбуке i7-3615qm общий быстрее, чем не общий.
См. http://ideone.com/Y1GIJK.
Ответ 8
Я думаю, что это ответ на мой вопрос, но какую магию я могу использовать, чтобы отрицать бокс?
Если ваша цель только для сравнения, вы можете сделать это:
public class A : IEquatable<A> {
public bool Equals( A other ) { return this == other; }
}
static bool F<T>( IEquatable<T> a, IEquatable<T> b ) where T : IEquatable<T> {
return a==b;
}
Это позволит избежать бокса.
Что касается основного отклонения по времени, я думаю, что все уже установили, что возникла проблема с настройкой секундомера. Я использую другую технику, где, если я хочу удалить сам цикл из результата времени, я беру пустой базовый уровень и просто вычитаю это из разницы времени. Это не идеально, но он дает справедливый результат и не замедляет запуск и остановку таймера снова и снова.