Пустой наследник Random работает медленнее родителя. string[] и object[] оказываются одним объектом. А Matrix4x4 до сих пор держит шестнадцать полей float вместо четырёх векторов.Ни одно из трёх уже не переделать, и на то есть причины. Читать далее
Пустой наследник Random меняет реализацию генератора. Массив строк хранится в переменной типа object[] до первой записи числа. А Matrix4×4 до сих пор работает на шестнадцати полях float вместо четырёх векторов.
Процессор | Система |
Intel Core i9-10900KF 3.70GHz, 10 ядер | Windows 10 22H2 |
AMD Ryzen 9 5950X 3.39GHz, 16 ядер | Windows 10 1809 |
Intel Xeon W-2255 3.70GHz, 10 ядер | Windows Server 2022 |
Intel Xeon Silver 4314 2.40GHz, 2 CPU, 32 ядра | Windows Server 2022 |
Все машины x64
Рантаймы.NET 8, 9 и 10 — все три в одном запуске BenchmarkDotNet 0.15.8.
1. Пустой наследник меняет реализациюИсходникПустой наследник:
internal sealed class EmptyRandom : Random
{
}
internal sealed class SeededRandom : Random
{
internal SeededRandom(int seed) : base(seed)
{
}
}Ни переопределённых методов, ни полей. И тем не менее конструктор Random подставит ему другую реализацию:
public Random()
{
// Note: if this is a derived type, the CompatDerivedImpl is used
_impl = GetType() == typeof(Random)
? new XoshiroImpl()
: new CompatDerivedImpl(this);
}Что выводит отчёт как создан реализация
new Random() XoshiroImpl
new EmptyRandom() Net5CompatDerivedImpl
new Random(42) Net5CompatSeedImpl
new SeededRandom(42) Net5CompatDerivedImpl
Первые 5 чисел с начальным значением 42:
Random 668 140 125 522 168
SeededRandom 668 140 125 522 168Числа те же. Отличается путь вызова.
Что показывает замерМетод | i9-10900KF | Ryzen 9 5950X | Xeon W-2255 | Xeon Silver 4314 |
Next | 1,816 / 2,608 | 2,333 / 3,190 | 2,318 / 3,138 | 3,196 / 4,104 |
NextDouble | 2,618 / 3,937 | 3,104 / 4,434 | 2,020 / 4,940 | 3,316 / 6,034 |
NextBytes, 256 байт | 37,025 / 537,487 | 35,885 / 572,608 | 50,093 / 648,626 | 51,063 / 889,873 |
Наносекунды, базовый тип и пустой наследник,.NET 10
На Next наследник отстаёт в 1,28–1,44 раза. На заполнении буфера — в 12,95–17,43 раза.
ПричинаПроверка в конструкторе нужна ради переопределённых методов. Скажем, наследник переопределил Sample. Тогда NextBytes обязан вызывать именно его, а не свою внутреннюю версию — иначе в старом коде числа поменяются.
Отличить один случай от другого в конструкторе невозможно: пустой наследник и наследник с переопределением выглядят одинаково. Поэтому все потомки получают Net5CompatDerivedImpl.
На буфере разница самая большая: Net5CompatDerivedImpl пишет по одному байту за раз, а XoshiroImpl — сразу по восемь.
Где легко ошибитьсяУнаследоваться, чтобы добавить один метод. Класс с единственным NextGuid получит Net5CompatDerivedImpl для всех остальных вызовов. Тот же самый, в котором живёт опечатка возрастом больше 20 лет.
Вместо наследования тут подходит метод расширения или обёртка с полем типа Random внутри.
2. string[] и object[] — один и тот же объектИсходникМассив строк можно присвоить переменной типа object[], и компилятор не выдаст ошибку:
string[] strings = new string[4];
object[] objects = strings;
objects[0] = "строка"; // работает
objects[0] = 42; // System.ArrayTypeMismatchExceptionЧто выводит отчёт objects.GetType() String[]
ReferenceEquals(strings, objects) True
objects[0] = "строка" прошло
objects[0] = 42 ArrayTypeMismatchException
new Span<object>(objects) ArrayTypeMismatchException
new Span<string>(strings) прошлоЭто один и тот же объект: ReferenceEquals возвращает true, а тип остаётся String[].
ПричинаМассивы ссылочных типов в.NET ковариантны: string[] считается наследником object[]. Так решили в первой версии: обобщённых типов тогда не было, и без ковариантности не получилось бы написать метод, работающий с массивом любого типа.
Цена — проверка при каждой записи. Рантайм сверяет тип значения с настоящим типом массива и при расхождении бросает исключение.
Из‑за этого Span при создании проверяет, что массив действительно того типа, что объявлен:
public Span(T[]? array)
{
if (array == null)
{
this = default;
return;
}
if (!typeof(T).IsValueType && array.GetType() != typeof(T[]))
{
ThrowHelper.ThrowArrayTypeMismatchException();
}
// ...
}Проверка нужна только для ссылочных типов: int[] никогда не окажется массивом другого типа.
Где легко ошибитьсяРешить, что эта проверка дорого обходится. Замер разницы не показал: 0,3966 наносекунды у string[] против 0,4034 у int[] на i9-10900KF, и так же на остальных трёх машинах. Тип массива известен в точке вызова, а потом джит проверку убирает.
Зато при записи в сам массив проверка остаётся. Поэтому массив один раз оборачивают в Span и уже с ним работают, так как в нём проверок при записи нет.
Причина указана в комментарии к Matrix4×4:
// In an ideal world, we'd have 4x Vector4 fields. However, Matrix4x4 was
// shipped with 16x public float fields and as such we cannot change the
// size or layout of the type without it being a breaking change.Что выводит отчёт тип размер открытых полей все поля float
Matrix4x4 64 байт 16 да
Matrix3x2 24 байт 6 да
Vector4 16 байт 4 да
Вложенные непубличные типы у Matrix4x4:
ImplПричинаЧетыре Vector4 укладываются в регистры процессора, и операции над матрицей выполняются векторными командами. Шестнадцать отдельных float так не умеют.
Поля публичные, к ним обращаются напрямую, а размер и раскладка структуры входят в двоичную совместимость. Их замена нарушает совместимость с уже написанным кодом.
Поэтому в соседнем файле объявили вложенную структуру Impl того же размера, но с четырьмя векторами, и приводят публичный тип к ней и обратно через Unsafe.As. В машинный код это приведение не попадает: типы одинаковы по размеру и раскладке, джит работает с тем же значением.
Что показывает замерДействие | i9-10900KF | Ryzen 9 5950X | Xeon W-2255 | Xeon Silver 4314 |
умножение 4×4 | 6,306 | 6,405 | 7,081 | 11,601 |
сложение 4×4 | 2,353 | 4,464 | 3,044 | 10,795 |
умножение 3×2 | 4,921 | 6,184 | 3,912 | 6,101 |
Наносекунды,.NET 10
Второй реализации Matrix4×4 в.NET нет, поэтому сравнивать эти числа не с чем. Четыре колонки показывают разброс по железу: на Xeon Silver 4314 те же операции идут почти вдвое дольше, чем на i9-10900KF.
Где легко ошибитьсяОбъявить публичные поля в структуре, которой будут пользоваться другие разработчики. После первого же релиза набор полей и их порядок становятся частью двоичной совместимости, и переписать их уже нельзя.
Со свойствами такого нет: внутри у них методы, а реализацию метода можно поменять в любой версии.
Границы замеровВсе замеры сняты на x64 под Windows, на.NET 8, 9 и 10.
У всех измеряемых методов запрещено встраивание. Наследник объявлен пустым, и сверка это подтверждает: у него нет ни одного объявленного метода.
Проверка типа массива в Span в замере не проявилась. Она в коде есть, но джит её выбрасывает, когда тип массива известен в точке вызова.
Код из статьиLegacyProof — замеры, отчёты и выгрузки с четырёх машин
Всем удачи и до новых встреч!
Только зарегистрированные пользователи могут участвовать в опросе. Войдите, пожалуйста.
6.67%Все знают1
6.67%Знал, но про разницу в 17 раз не думал1
86.67%А чё, так можно было что ли!13
Проголосовали 15 пользователей. Воздержались 3 пользователя.
| # | Наименование новости | Тональность | Информативность | Дата публикации |
|---|---|---|---|---|
| 1 | А чё, так можно было? Три недооценённых атрибута.NET | 0 | 9.91 | 14-09-2026 |
| 2 | А чё, так можно было? Опечатка в исходниках .NET, которой больше 20 лет | 0 | 14.73 | 15-09-2026 |
| 3 | .NET Matrix: взвешенный выбор библиотеки, а не по звёздам на GitHub | 0 | 9.2 | 30-07-2026 |
| 4 | Бенчмаркая try/finally: один finally — и метод в 6,45 раза медленнее | 0 | 8.98 | 17-08-2026 |
| 5 | Бенчмаркая Sum: ускорил циклом — замедлил в ×4,7 | 0 | 7.81 | 18-07-2026 |
| 6 | Отбор акций на .NET: 14,5 млн баров в SQLite, фильтр, который мерил не в тех единицах, и перестановочный тест | 0 | 7.94 | 11-09-2026 |
| 7 | Обзор нововведений в C# 15 | 0 | 8.55 | 21-10-2026 |
| 8 | Native AOT заставил меня переписать весь .NET проект | 0 | 8.36 | 22-09-2026 |
| 9 | Зачем сетевику может понадобиться язык «C» | 0 | 9.34 | 26-08-2026 |
| 10 | 3000 точек на карте грузились полсекунды. Ускорял не там, где думал | 0 | 10.75 | 21-08-2026 |