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