Как этот вызов виртуального метода быстрее, чем вызов закрытого метода? - PullRequest
4 голосов
/ 10 декабря 2010

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

Ниже приведен мой тестовый код.

Выход

virtual total 3166ms
per call virtual 3.166ns
sealed total 3931ms
per call sealed 3.931ns

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

Я работаю в режиме выпуска с включенным «Оптимизировать код».

Редактировать: при работе вне VS (в качестве консольного приложения) время близко к абсолютному перегреву. но виртуал почти всегда выходит вперед.

[TestFixture]
public class VirtTests
{

    public class ClassWithNonEmptyMethods
    {
        private double x;
        private double y;

        public virtual void VirtualMethod()
        {
            x++;
        }
        public void SealedMethod()
        {
            y++;
        }
    }

    const int iterations = 1000000000;


    [Test]
    public void NonEmptyMethodTest()
    {

        var foo = new ClassWithNonEmptyMethods();
        //Pre-call
        foo.VirtualMethod();
        foo.SealedMethod();

        var virtualWatch = new Stopwatch();
        virtualWatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            foo.VirtualMethod();
        }
        virtualWatch.Stop();
        Console.WriteLine("virtual total {0}ms", virtualWatch.ElapsedMilliseconds);
        Console.WriteLine("per call virtual {0}ns", ((float)virtualWatch.ElapsedMilliseconds * 1000000) / iterations);


        var sealedWatch = new Stopwatch();
        sealedWatch.Start();
        for (var i = 0; i < iterations; i++)
        {
            foo.SealedMethod();
        }
        sealedWatch.Stop();
        Console.WriteLine("sealed total {0}ms", sealedWatch.ElapsedMilliseconds);
        Console.WriteLine("per call sealed {0}ns", ((float)sealedWatch.ElapsedMilliseconds * 1000000) / iterations);

    }

}

Ответы [ 3 ]

4 голосов
/ 11 декабря 2010

Вы тестируете влияние выравнивания памяти на эффективность кода. 32-битному JIT-компилятору не удается создать эффективный код для типов значений, размер которых превышает 32 бита, длинный и двойной в коде C #. Корень проблемы - 32-битный распределитель кучи GC, он обещает выравнивание выделенной памяти только по адресам, кратным 4. Это проблема, вы увеличиваете удвоения. Двойной код эффективен только тогда, когда он выровнен по адресу, кратному 8. Такая же проблема со стеком, в случае локальных переменных он также выравнивается только до 4 на 32-разрядном компьютере.

Кэш ЦП L1 внутренне организован в блоки, называемые «линия кеша». Существует штраф, когда программа читает неправильно выровненный дубль. Особенно тот, который охватывает конец строки кэша, байты из двух строк кэша должны быть прочитаны и склеены вместе. Неправильное выравнивание не является редкостью в 32-битном джиттере, это всего лишь 50-50 шансов, что поле 'x' будет выделено по адресу, кратному 8. Если это не так, то 'x' и 'y' будет неправильно выровнен, и один из них вполне может занять строчку кэша. То, как вы написали тест, будет замедлять VirtualMethod или SealedMethod. Убедитесь, что вы позволили им использовать одно и то же поле для получения сопоставимых результатов.

То же самое относится и к коду. Поменяйте местами код для виртуального и запечатанного теста, чтобы произвольно изменить результат. У меня не было проблем с тем, чтобы сделать запечатанный тест немного быстрее. Учитывая скромную разницу в скорости, вы, вероятно, смотрите на проблему с выравниванием кода. Джиттер x64 пытается вставить NOP, чтобы выровнять цель ветвления, а джиттер x86 - нет.

Вам также следует запускать проверку синхронизации несколько раз в цикле, по крайней мере, 20. Затем вы, вероятно, также заметите, как сборщик мусора перемещает объект класса. У двойника может быть другое выравнивание впоследствии, резко изменяя выбор времени. Доступ к 64-битному значению типа значения, например long или double, имеет 3 различных момента времени, выровненных по 8, выровненных по 4 в пределах строки кэша и выровненных по 4 по двум строкам кэша. В быстром или медленном порядке.

Штраф круче, чтение двойного, который пересекает строку кэша, примерно в в три раз медленнее, чем чтение выровненного. Кроме того, основная причина, почему double [] (массив значений типа double) выделяется в куче больших объектов, даже если он содержит только 1000 элементов, к югу от нормального порога в 80 КБ, у LOH есть гарантия выравнивания 8. Эти проблемы выравнивания полностью исчезают в коде, генерируемом джиттером x64, и стек, и куча GC имеют выравнивание 8.

1 голос
/ 11 декабря 2010

Возможно, вы видите начальную стоимость.Попробуйте обернуть код Test-A / Test-B в цикл и запустить его несколько раз.Вы также можете увидеть какие-то эффекты упорядочения.Чтобы избежать этого (и верха / низа петлевых эффектов), разверните его 2-3 раза.

1 голос
/ 10 декабря 2010

Сначала необходимо отметить метод sealed.

Во-вторых, укажите override для виртуального метода. Создайте экземпляр производного класса.

В качестве третьего теста создайте метод sealed override.

Теперь вы можете начать сравнение.

Редактировать: Вероятно, вы должны запустить это вне VS.

Обновление:

Пример того, что я имею в виду.

abstract class Foo
{
  virtual void Bar() {}
}

class Baz : Foo
{
  sealed override void Bar() {}
}

class Woz : Foo
{
  override void Bar() {}
}

Теперь проверьте скорость вызова Bar для экземпляра Baz и Woz. Я также подозреваю, что видимость элементов и классов вне сборки может повлиять на анализ JIT.

...