Страницы

Поиск по вопросам

понедельник, 30 марта 2020 г.

sealed, virtual, невиртуальные методы в C# и производительность

#c_sharp #net #ооп #производительность #clr


Стоит задача максимального увеличения производительности в определенной части приложения.
Почитав некоторые статьи хабра и где-то когда-то что-то слышав или читав, принялся,
кроме всего прочего, запечатывать классы и методы (а также превращать некоторые небольшие
классы в структуры). 
Уже закоммитив, решил в небольшом приложении проверить, а действительно ли это дает
результат.

class X
{
    public int NonVirtual() => DateTime.Now.Millisecond;
    public virtual int Virtual() => DateTime.Now.Minute;
}

class Y : X
{
    public override int Virtual() => DateTime.Now.Millisecond;
}

class Program
{

    private static volatile int TST = 0;

    static int Slow(Y x) => x.Virtual();

    static int Fast(Y y) => y.NonVirtual();

    static void Main(string[] args)
    {

        int i = 0;
        var stopwatch1 = new Stopwatch();
        var stopwatch2 = new Stopwatch();

        stopwatch1.Start();
        var y = new Y();
        for (i = 0; i < 100000000; i++)
        {
            TST = Fast(y);
        }
        stopwatch1.Stop();
        Console.WriteLine("Time elapsed: {0}", stopwatch1.Elapsed);

        stopwatch2.Start();
        for (i = 0; i < 100000000; i++)
        {
            TST = Slow(y);
        }
        stopwatch2.Stop();
        Console.WriteLine("Time elapsed: {0}", stopwatch2.Elapsed);

        Console.ReadKey();
    }
}


Поле TST объявлено как volatile, чтобы компилятор не оптимизировал вызовы.

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

Тогда я полез в IL:

IL_0001: callvirt  instance int32 PerformanceTests.X::Virtual()


Это вызов виртуального метода. Вроде всё нормально. Второй вызов:

IL_0001: callvirt  instance int32 PerformanceTests.X::NonVirtual()


Что, простите? callvirt? Разве тут не должно быть call? sealed так-же никак не влияет
на вызов виртуального метода.

Хотел бы выяснить у более опытных коллег, всё-таки, есть ли смысл в запечатывании
классов в плане производительности? А так-же, почему вызов невиртуальной функции в
IL такой же как и виртуальной?

Update:
В комментариях @Grundy написал, что callvirt - из-за того, что метод объявлен в базовом
классе. Переписал код так, что теперь используется базовый класс (т.е. Y - не используется).
callvirt так и вызывается.
    


Ответы

Ответ 1



Как уже сказали в комментариях, компилятор использует callvirt что бы генерировать NullReferenceException. Что бы получить чистую call инструкцию компилятор должен быть уверен, что экземпляр класса не может быть null. Пример: class Test { public void Method() => Console.WriteLine(123); } static void Main(string[] args) { new Test().Method(); } IL-код: IL_0001: newobj instance void ConsoleApp1.Program/Test::.ctor() IL_0006: call instance void ConsoleApp1.Program/Test::Method() Если код немного изменить: static Test GetTest() => new Test(); static void Main(string[] args) { GetTest().Method(); } То уже получаем callvirt, так как компилятор предполает, что GetTest может вернуть null: IL_0001: call class ConsoleApp1.Program/Test ConsoleApp1.Program::GetTest() IL_0006: callvirt instance void ConsoleApp1.Program/Test::Method() sealed ни на что не влияет в рантайме. Это просто маркер для разработчиков, который сообщает, что можно делать в высокоуровневом коде, а что нельзя. Для каждого callvirt невиртуального метода JIT вставляет одну дополнительную инструкцию перед каждым call: cmp dword ptr [/*здесь регистр с адресом экземляра*/],ecx call 00007FF9CD540098 // метод Эффект от одной cmp инструкции очень незначителен.

Комментариев нет:

Отправить комментарий