C #: вызов виртуальной функции даже быстрее, чем вызов делегата? - PullRequest
13 голосов
/ 19 октября 2008

Со мной случается только один вопрос о дизайне кода. Скажем, у меня есть один «шаблонный» метод, который вызывает некоторые функции, которые могут «изменять». Интуитивно понятный дизайн должен следовать шаблону Design Pattern. Определите изменяющиеся функции как «виртуальные» функции, которые будут переопределены в подклассах. Или я могу просто использовать функции делегата без "виртуального". Функции делегата внедряются так, что они также могут быть настроены.

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

В приведенном ниже коде первый метод DoSomething следует «шаблону шаблона». Он вызывает виртуальный метод IsTokenChar. Второй метод DoSomthing не зависит от виртуальной функции. Вместо этого у него есть делегат передачи. В моем компьютере первый DoSomthing всегда быстрее, чем второй. Результат как 1645: 1780.

«Виртуальный вызов» является динамическим связыванием и должен стоить больше времени, чем прямой вызов делегирования, верно? но результат показывает, что это не так.

Кто-нибудь может это объяснить?

using System;
using System.Diagnostics;

class Foo
{
    public virtual bool IsTokenChar(string word)
    {
        return String.IsNullOrEmpty(word);
    }

    // this is a template method
    public int DoSomething(string word)
    {
        int trueCount = 0;
        for (int i = 0; i < repeat; ++i)
        {
            if (IsTokenChar(word))
            {
                ++trueCount;
            }
        }
        return trueCount;
    }

    public int DoSomething(Predicate<string> predicator, string word)
    {
        int trueCount = 0;
        for (int i = 0; i < repeat; ++i)
        {
            if (predicator(word))
            {
                ++trueCount;
            }
        }
        return trueCount;
    }

    private int repeat = 200000000;
}

class Program
{
    static void Main(string[] args)
    {
        Foo f = new Foo();

        {
            Stopwatch sw = Stopwatch.StartNew();
            f.DoSomething(null);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }

        {
            Stopwatch sw = Stopwatch.StartNew();
            f.DoSomething(str => String.IsNullOrEmpty(str), null);
            sw.Stop();
            Console.WriteLine(sw.ElapsedMilliseconds);
        }
    }
}

Ответы [ 7 ]

19 голосов
/ 19 октября 2008

Подумайте, что требуется в каждом случае:

Виртуальный звонок

  • Проверка на ничтожность
  • Переход от указателя объекта к указателю типа
  • Поиск адреса метода в таблице инструкций
  • (Не уверен - даже Рихтер не покрывает это) Перейти к базовому типу, если метод не переопределен? Повторяйте, пока мы не найдем правильный адрес метода. (Я так не думаю - см. Правку внизу.)
  • Вставить оригинальный указатель объекта в стек («this»)
  • Метод вызова

Делегатский звонок

  • Проверка на ничтожность
  • Переход от указателя объекта к массиву вызовов (все делегаты потенциально многоадресные)
  • Цикл по массиву и для каждого вызова:
    • Адрес метода извлечения
    • Определите, следует ли передавать цель в качестве первого аргумента
    • Вставить аргументы в стек (возможно, это уже было сделано - не уверен)
    • По выбору (в зависимости от того, открыт или закрыт вызов) помещает цель вызова в стек
    • Метод вызова

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

Но в основном с делегатом связано столько же косвенного обращения. Учитывая, что я не уверен в вызове виртуального метода, вполне возможно, что вызов не переопределенного виртуального метода в иерархии с очень глубокими типами будет медленнее ... Я попробую и отредактирую ответ.

РЕДАКТИРОВАТЬ: я пытался поиграть как с глубиной иерархии наследования (до 20 уровней), с точки зрения "наиболее производного переопределения", так и с объявленным типом переменной - и ни один из них, похоже, не имеет значения.

РЕДАКТИРОВАТЬ: Я только что попробовал оригинальную программу с использованием интерфейса (который передается в) - в итоге производительность примерно такая же, как у делегата.

12 голосов
/ 06 февраля 2009

Просто хотел добавить несколько исправлений в ответ Джона Скита:

При вызове виртуального метода не требуется выполнять проверку на ноль (автоматически обрабатывается аппаратными прерываниями).

Также не нужно проходить по цепочке наследования, чтобы найти не переопределенные методы (для этого и нужна таблица виртуальных методов).

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

Вызов делегата также включает дополнительный уровень косвенности.

Вызовы делегату не предполагают помещения аргументов в массив, если вы не выполняете динамический вызов с использованием метода DynamicInvoke.

Вызов делегата включает вызывающий метод, вызывающий сгенерированный компилятором метод Invoke для рассматриваемого типа делегата. Вызов предикатора (значение) превращается в предикатор. Invoke (значение).

Метод Invoke, в свою очередь, реализуется JIT для вызова указателя (ей) функции (хранится внутри объекта делегата).

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

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

Разница может быть связана с необходимостью дополнительных проверок + ветвей из-за многоадресной рассылки (как предложил Джон). Другой причиной может быть то, что JIT-компилятор не встроен в метод Delegate.Invoke, а реализация Delegate.Invoke не обрабатывает аргументы, а также реализацию при выполнении вызовов виртуальных методов.

8 голосов
/ 19 октября 2008

Виртуальный вызов разыменовывает два указателя с известным смещением в памяти. Это на самом деле не динамическое связывание; во время выполнения нет кода, который можно было бы отразить в метаданных, чтобы найти правильный метод. Компилятор генерирует пару инструкций для выполнения вызова на основе указателя this. фактически виртуальный вызов - это одиночная инструкция IL.

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

Я бы посоветовал вам взглянуть на конструкции IL для обоих. Скомпилируйте упрощенную версию вышеупомянутого источника с помощью одного вызова для каждого из двух DoSomthing. Затем используйте ILDASM, чтобы увидеть, что является реальным кодом для каждого шаблона.

(И я уверен, что меня опровергнут за неправильную терминологию: -))

3 голосов
/ 26 июня 2009

Результат теста стоит 1000 слов: http://kennethxu.blogspot.com/2009/05/strong-typed-high-performance_15.html

1 голос
/ 19 октября 2008

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

Обратите внимание, что согласно этой статье блога разница была еще больше в .NET v1.x.

1 голос
/ 19 октября 2008

Возможно, что, поскольку у вас нет методов, переопределяющих виртуальный метод, JIT может распознать это и использовать вместо этого прямой вызов.

Для чего-то подобного, как правило, лучше проверить это, чем вы, чем пытаться угадать, какой будет производительность. Если вы хотите узнать больше о том, как работает вызов делегата, я предлагаю отличную книгу Джеффри Рихтера "CLR Via C #".

0 голосов
/ 19 октября 2008
Виртуальные переопределения

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

Делегаты являются динамическими, которые всегда будут иметь накладные расходы, и они тоже кажутся объектами, так что это складывается.

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

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...