Подтвердить что ты не робот

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

Я написал два эквивалентных метода:

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.

4b9b3361

Ответ 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;
    }

Это позволит избежать бокса.

Что касается основного отклонения по времени, я думаю, что все уже установили, что возникла проблема с настройкой секундомера. Я использую другую технику, где, если я хочу удалить сам цикл из результата времени, я беру пустой базовый уровень и просто вычитаю это из разницы времени. Это не идеально, но он дает справедливый результат и не замедляет запуск и остановку таймера снова и снова.