Как избежать накладных расходов на виртуальные вызовы C # - PullRequest
0 голосов
/ 14 декабря 2018

У меня есть несколько сильно оптимизированных математических функций, для выполнения которых требуется 1-2 nanoseconds.Эти функции вызываются сотни миллионов раз в секунду, поэтому издержки вызова представляют собой проблему, несмотря на и без того превосходную производительность.

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

public interface IMathFunction
{
  double Calculate(double input);
  double Derivate(double input);
}

public SomeObject
{
  // Note: There are cases where this is mutable
  private readonly IMathFunction mathFunction_; 

  public double SomeWork(double input, double step)
  {
    var f = mathFunction_.Calculate(input);
    var dv = mathFunction_.Derivate(input);
    return f - (dv * step);
  }
}

Этот интерфейс вызывает огромные накладные расходы по сравнению с прямым вызовом из-за того, как его использует код потребления.Прямой вызов занимает 1-2 нс , тогда как вызов виртуального интерфейса занимает 8-9 нс .Очевидно, что наличие интерфейса и его последующая трансляция виртуального вызова является узким местом для этого сценария.

Я хотел бы сохранить как удобство обслуживания, так и производительность, если это возможно. Есть ли способ разрешить виртуальную функцию для прямого вызова, когда создается экземпляр объекта, чтобы все последующие вызовы могли избежать накладных расходов? Я предполагаю, что это потребовало бы создания делегатов с IL, но я бы не сталне знаю, с чего начать.

Ответы [ 2 ]

0 голосов
/ 14 декабря 2018

Я бы назначил методы для делегатов.Это позволяет вам все равно программировать на интерфейсе, избегая при этом разрешения метода интерфейса.

public SomeObject
{
    private readonly Func<double, double> _calculate;
    private readonly Func<double, double> _derivate;

    public SomeObject(IMathFunction mathFunction)
    {
        _calculate = mathFunction.Calculate;
        _derivate = mathFunction.Derivate;
    }

    public double SomeWork(double input, double step)
    {
        var f = _calculate(input);
        var dv = _derivate(input);
        return f - (dv * step);
    }
}

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

Результаты теста (среднее время 100 миллионов итераций в нс) с пустым временем метода, вычтенным в фигурных скобках:

Пусто Метод работы: 1,48
Интерфейс: 5,69 (4,21)
Делегаты: 5,78 (4,30)
Запечатанный класс: 2,10 (0,62)
Класс: 2,12 (0,64)

Время версии делегата примерно такое же, как и для версии интерфейса (точное время варьируется от выполнения теста к выполнению теста).В то время как работа с классом примерно в 6,8 раза быстрее (сравнение времени минус время пустого метода работы)!Это означает, что мое предложение работать с делегатами не помогло!

Меня удивило то, что я ожидал гораздо более продолжительного времени выполнения версии интерфейса.Поскольку этот вид теста не представляет точный контекст кода OP, его валидность ограничена.

static class TimingInterfaceVsDelegateCalls
{
    const int N = 100_000_000;
    const double msToNs = 1e6 / N;

    static SquareFunctionSealed _mathFunctionClassSealed;
    static SquareFunction _mathFunctionClass;
    static IMathFunction _mathFunctionInterface;
    static Func<double, double> _calculate;
    static Func<double, double> _derivate;

    static TimingInterfaceVsDelegateCalls()
    {
        _mathFunctionClass = new SquareFunction();
        _mathFunctionClassSealed = new SquareFunctionSealed();
        _mathFunctionInterface = _mathFunctionClassSealed;
        _calculate = _mathFunctionInterface.Calculate;
        _derivate = _mathFunctionInterface.Derivate;
    }

    interface IMathFunction
    {
        double Calculate(double input);
        double Derivate(double input);
    }

    sealed class SquareFunctionSealed : IMathFunction
    {
        public double Calculate(double input)
        {
            return input * input;
        }

        public double Derivate(double input)
        {
            return 2 * input;
        }
    }

    class SquareFunction : IMathFunction
    {
        public double Calculate(double input)
        {
            return input * input;
        }

        public double Derivate(double input)
        {
            return 2 * input;
        }
    }

    public static void Test()
    {
        var stopWatch = new Stopwatch();

        stopWatch.Start();
        for (int i = 0; i < N; i++) {
            double result = SomeWorkEmpty(i);
        }
        stopWatch.Stop();
        double emptyTime = stopWatch.ElapsedMilliseconds * msToNs;
        Console.WriteLine($"Empty Work method: {emptyTime:n2}");

        stopWatch.Restart();
        for (int i = 0; i < N; i++) {
            double result = SomeWorkInterface(i);
        }
        stopWatch.Stop();
        PrintResult("Interface", stopWatch.ElapsedMilliseconds, emptyTime);

        stopWatch.Restart();
        for (int i = 0; i < N; i++) {
            double result = SomeWorkDelegate(i);
        }
        stopWatch.Stop();
        PrintResult("Delegates", stopWatch.ElapsedMilliseconds, emptyTime);

        stopWatch.Restart();
        for (int i = 0; i < N; i++) {
            double result = SomeWorkClassSealed(i);
        }
        stopWatch.Stop();
        PrintResult("Sealed Class", stopWatch.ElapsedMilliseconds, emptyTime);

        stopWatch.Restart();
        for (int i = 0; i < N; i++) {
            double result = SomeWorkClass(i);
        }
        stopWatch.Stop();
        PrintResult("Class", stopWatch.ElapsedMilliseconds, emptyTime);
    }

    private static void PrintResult(string text, long elapsed, double emptyTime)
    {
        Console.WriteLine($"{text}: {elapsed * msToNs:n2} ({elapsed * msToNs - emptyTime:n2})");
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static double SomeWorkEmpty(int i)
    {
        return 0.0;
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static double SomeWorkInterface(int i)
    {
        double f = _mathFunctionInterface.Calculate(i);
        double dv = _mathFunctionInterface.Derivate(i);
        return f - (dv * 12.34534);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static double SomeWorkDelegate(int i)
    {
        double f = _calculate(i);
        double dv = _derivate(i);
        return f - (dv * 12.34534);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static double SomeWorkClassSealed(int i)
    {
        double f = _mathFunctionClassSealed.Calculate(i);
        double dv = _mathFunctionClassSealed.Derivate(i);
        return f - (dv * 12.34534);
    }

    [MethodImpl(MethodImplOptions.NoInlining)]
    private static double SomeWorkClass(int i)
    {
        double f = _mathFunctionClass.Calculate(i);
        double dv = _mathFunctionClass.Derivate(i);
        return f - (dv * 12.34534);
    }
}

Идея [MethodImpl(MethodImplOptions.NoInlining)] состоит в том, чтобы запретить компилятору вычислять адреса методов до того, какцикл, если метод был встроен.

0 голосов
/ 14 декабря 2018

Так что это имеет очевидные ограничения и не должно использоваться постоянно, где бы у вас ни был интерфейс, но если у вас есть место, где перфом действительно нужно максимизировать, вы можете использовать дженерики:

public SomeObject<TMathFunction> where TMathFunction: struct, IMathFunction 
{
  private readonly TMathFunction mathFunction_;

  public double SomeWork(double input, double step)
  {
    var f = mathFunction_.Calculate(input);
    var dv = mathFunction_.Derivate(input);
    return f - (dv * step);
  }
}

Ивместо передачи интерфейса передайте свою реализацию как TMathFunction.Это позволит избежать поиска в vtable из-за интерфейса, а также позволит выполнять вставку.

Обратите внимание, что здесь важно использовать struct, так как в общем случае дженерики будут обращаться к классу через интерфейс.

Некоторая реализация:

Я сделал простую реализацию IMathFunction для тестирования:

class SomeImplementationByRef : IMathFunction
{
    public double Calculate(double input)
    {
        return input + input;
    }

    public double Derivate(double input)
    {
        return input * input;
    }
}

... а также версию структуры и абстрактную версию.

Итак, вот что происходит с версией интерфейса.Вы можете видеть, что он относительно неэффективен, потому что выполняет два уровня косвенности:

    return obj.SomeWork(input, step);
sub         esp,40h  
vzeroupper  
vmovaps     xmmword ptr [rsp+30h],xmm6  
vmovaps     xmmword ptr [rsp+20h],xmm7  
mov         rsi,rcx
vmovsd      qword ptr [rsp+60h],xmm2  
vmovaps     xmm6,xmm1
mov         rcx,qword ptr [rsi+8]          ; load mathFunction_ into rcx.
vmovaps     xmm1,xmm6  
mov         r11,7FFED7980020h              ; load vtable address of the IMathFunction.Calculate function.
cmp         dword ptr [rcx],ecx  
call        qword ptr [r11]                ; call IMathFunction.Calculate function which will call the actual Calculate via vtable.
vmovaps     xmm7,xmm0
mov         rcx,qword ptr [rsi+8]          ; load mathFunction_ into rcx.
vmovaps     xmm1,xmm6  
mov         r11,7FFED7980028h              ; load vtable address of the IMathFunction.Derivate function.
cmp         dword ptr [rcx],ecx  
call        qword ptr [r11]                ; call IMathFunction.Derivate function which will call the actual Derivate via vtable.
vmulsd      xmm0,xmm0,mmword ptr [rsp+60h] ; dv * step
vsubsd      xmm7,xmm7,xmm0                 ; f - (dv * step)
vmovaps     xmm0,xmm7  
vmovaps     xmm6,xmmword ptr [rsp+30h]  
vmovaps     xmm7,xmmword ptr [rsp+20h]  
add         rsp,40h  
pop         rsi  
ret  

Вот абстрактный класс.Это немного более эффективно, но незначительно:

        return obj.SomeWork(input, step);
 sub         esp,40h  
 vzeroupper  
 vmovaps     xmmword ptr [rsp+30h],xmm6  
 vmovaps     xmmword ptr [rsp+20h],xmm7  
 mov         rsi,rcx  
 vmovsd      qword ptr [rsp+60h],xmm2  
 vmovaps     xmm6,xmm1  
 mov         rcx,qword ptr [rsi+8]           ; load mathFunction_ into rcx.
 vmovaps     xmm1,xmm6  
 mov         rax,qword ptr [rcx]             ; load object type data from mathFunction_.
 mov         rax,qword ptr [rax+40h]         ; load address of vtable into rax.
 call        qword ptr [rax+20h]             ; call Calculate via offset 0x20 of vtable.
 vmovaps     xmm7,xmm0  
 mov         rcx,qword ptr [rsi+8]           ; load mathFunction_ into rcx.
 vmovaps     xmm1,xmm6  
 mov         rax,qword ptr [rcx]             ; load object type data from mathFunction_.
 mov         rax,qword ptr [rax+40h]         ; load address of vtable into rax.
 call        qword ptr [rax+28h]             ; call Derivate via offset 0x28 of vtable.
 vmulsd      xmm0,xmm0,mmword ptr [rsp+60h]  ; dv * step
 vsubsd      xmm7,xmm7,xmm0                  ; f - (dv * step)
 vmovaps     xmm0,xmm7
 vmovaps     xmm6,xmmword ptr [rsp+30h]  
 vmovaps     xmm7,xmmword ptr [rsp+20h]  
 add         rsp,40h  
 pop         rsi  
 ret  

Таким образом, и интерфейс, и абстрактный класс в значительной степени полагаются на предсказание цели перехода для достижения приемлемой производительности.Даже в этом случае вы можете увидеть, что в это дело входит гораздо больше, поэтому лучший вариант все еще относительно медленный, а худший случай - остановленный конвейер из-за неправильного прогноза.

И, наконец, вот общая версиясо структурой.Вы можете видеть, что он значительно эффективнее, потому что все полностью встроено, поэтому прогноз ветвления не задействован.У него также есть приятный побочный эффект - удаление большей части управления стека / параметров, которое было там, поэтому код становится очень компактным:

    return obj.SomeWork(input, step);
push        rax  
vzeroupper  
movsx       rax,byte ptr [rcx+8]  
vmovaps     xmm0,xmm1  
vaddsd      xmm0,xmm0,xmm1  ; Calculate - got inlined
vmulsd      xmm1,xmm1,xmm1  ; Derivate - got inlined
vmulsd      xmm1,xmm1,xmm2  ; dv * step
vsubsd      xmm0,xmm0,xmm1  ; f - 
add         rsp,8  
ret  
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...