Pull to refresh

Comments 32

Миф развенчали, но есть вопросы.
Почему не 16 а 64?
Справедливо ли ваше утверждение для 32-битных процессоров?
Что дороже скопировать ссылку или структуру такого же размера?

Справедливо ли ваше утверждение для 32-битных процессоров?

И для ARM.

Почему не 16 а 64?

Признаться, я не копал в эту сторону. Для себя решил, что это текущие детали реализации JIT-компилятора.

Справедливо ли ваше утверждение для 32-битных процессоров?

Вот это, как и другие вопросы в комментариях, как говорится, совсем другая история *Леонид Каневский.жпг* Я не просто так в начале статьи написал дисклеймер. :) На каждый из вопросов, по-сути, надо делать отдельный бенчмарк и сравнивать. Когда комментарии настоятся, то, возможно, сделаю ещё один пост с ответами на вопросы.

Суть использования ссылок, это многократное использование объекта. Объект 1 раз создался и многократно может быть передан ссылкой. То есть на системе х64 ссылка весит 8 байт. Копирование ссылки в 8 байт не может быть дороже копирования структуры 64 байта. Хотелось бы понять, что вы замерили? От прочтения статьи у меня сложилось впечатление, что использовать структуры до 64 байты выгоднее, но так ли это?
У 16 байт есть вполне адекватное объяснение, это 8 байт ссылки + накладные расходы на обращение к какому-то полю, как обосновать 64?

Копирование ссылки в 8 байт не может быть дороже копирования структуры 64 байта. Хотелось бы понять, что вы замерили?

Я не утверждаю, что копирование 64 байт быстрее копирования 8 байт. Я показал, как использование структур определённого размера не ухудшает производительность. Достигается это за счёт оптимизаций. Это разные вещи.

Можете попробовать запустить бенчмарк у себя и поизучать. Я приложил ссылку на GitHub с кодом.

Вы правы в том, что копировать 8 байт быстрее, но как быть, если вам надо прочитать объект из памяти по адресу, осуществить обработку и таких объектов у вас миллионы. Если это будет храниться в структурах, то платформа разместит это все рядом, на соседних страницах памяти. Значит в кэш процессора будут попадать данные, которые, скорее всего вам понадобятся на следующих итерациях, значит процессор не будет лезть в RAM и возьмет данные из кэша. Доступ к кэшу мгновенный. Это и есть основное преимущество значимых типов в .net. Если использовать ссылочный типы, они могут быть разбросаны по разным блокам памяти, и доступ к ним существенно медленнее, и будет вести к фрагментации памяти и вызовам GC. Тем не менее, среда довольно умная и может эта исправить с помощью сжатия (дефрагментации) памяти.

Ссылки берутся из тн "кучи", а значения из стэка. (если не занудствовать)

Из кучи берётся не сама ссылка, а значение на которую она указывает - это раз. Сама ссылка лежит на стеке - это два.

И три - ссылка вполне может указывать на стек (при передаче через in/ref/out)

Четыре: ссылка может быть и не в стеке, а в регистре.

Предположительно, в примере структуры до 64 байт могут работать быстрее, т.к. используемый dotnet runtime умеет все значения этих структур размещать в регистрах данного процессора, а достаточно простые функции при этом встраивать в вызывающий код.
При параметрах функции более 64 байт, очевидно, их передачу уже приходится осуществлять через стек, что гораздо медленнее.

Вы копали вообще не в ту сторону. В вашем тесте смешано и заменено вообще всё, что можно.

1) в нем тестируется аллокация тоже, что в корне не верно. Надо тестировать лишь передачу параметров. Хотя, кстати, в оригинальных guidelines от MS вообще ничего не сказано про передачу параметров. Но раз уж вы захотели тестировать именно это, то надо исключить создание объекта.

2) в тесте используются лямбды, которые транслируются инлайн почти всегда (или вообще всегда?). Для чистого теста надо было объявить внешние функции (в другом файле?).

3) но в чем, собственно, вопрос? Даже структуру на 128 байт выгоднее использовать, если она количество передач ее в параметре не сильно больше количества созданий собственно структуры. То есть, она выигрывает у 128-байтного класса на создании, потому что не лезет/меняет хип при создании/удалении (а ваш тест удаление классов вообще не затрагивает, наверное).

Вангую: 64 байта - CPU cache entry size (cache line).
Разница в производительности - цена (время) копирования из кеша в память.
Если кто то сможет запустить тест на CPU c 32 байт cache line - подтвердим гипотезу.

Одним таким тестом не отделаешься, даже если дотнетовская VM на эту платформу портирована, то реализация (качество) JIT может очень сильно отличаться.

Благодарю. Было интересно посмотреть на аткульные "лимиты" структур.
Но данные, вероятно, будут разными под разные платформы (win/linux/arm/x86/x64) в силу различий VM.

каков будет результат если принудительно отключить инлайн методов? в реальной жизни далеко не все методы инлайнятся.

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

Кроме того они ещё и как record помечены, то бишь иммутабельные записи, так что там ещё некоторый простор с оптимизацией во время компиляции существует с такой границей .

Структуры-record'сы не помечены readonly, так что иммутабельны здесь только классы. Но мне вот стало интересно на что использование record'сов влияет под капотом.

Не знаток C#, но мне казалось, что record завели для как раз таки компайл-тайм инварианта, позволяя как оптимизацию исполнения так и расположения в памяти. Несколько странно, конечно, что их разрешают делать мутабельными.

While records can be mutable, they're primarily intended for supporting immutable data models

ref

UFO just landed and posted this here

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

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

А какую гипотезу-то проверяли? Пока выглядит будто проверяли скорость работы вектора на разных размерах данных.

А рекомендации относятся к копированию при вызове метода.

То есть бенчмарки не согласуются с гипотезой от слова никак.

А как на вызов метода это влияет кроме миниатюрного выигрыша на загрузке памяти в кэш, если метод обрабатывает всю структупу?

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

Ну хоть кто-то здесь адекватный.

Интересно. Но всё таки си шар используют для повышения скорости разработки и такие тонкие вопросы оптимизации в нём возникают крайне редко. Обычно оптимизируется то, что даёт прирост на порядок.

В сравнении CIL кода показаны методы GetClass16/64, а не GetStruct16/64

Эта оптимизация известна под названием function inlining или method inlining. Благодаря этой оптимизации, CLR не пришлось копировать экземпляр структуры при возврате из метода.

Ну так и ваш тест тогда некорректен, потому что он не тестирует собственно правило 16 байт, т.е. разницу в скорости копирования возвращаемого значения, потому что метод заинлайнился и ничего не возвращается. Оно так же и в 2008 работало скорее всего. Необходимо пометить методы атрибутом [MethodImpl(MethodImplOptions.NoInlining)]

Я не утверждал в статье, что 64 байта копируется быстрее. Я написал, что структуры до 64 байт не ухудшают производительность. Да, это происходит при определённых условиях (смотрим дисклеймер), да за счёт оптимизаций, но это всё же происходит. Но, спасибо за рекомендацию. В комментах уже встречался совет проверить без инлайна. Пожалуй, проверю и сделаю отдельную публикацию, т.к. самому интересно. :)

public record struct Class04(int Param);

Похоже что ппечатка.

Sign up to leave a comment.

Articles