#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 инструкции очень незначителен.
Комментариев нет:
Отправить комментарий