Комментарии 35
Еще один способ избежать боксинга/анбоксинга в дженерик методах это TypedReferences(работают в моно с 2.10)
static void foo(ref T value)
{
//This is the ONLY way to treat value as int, without boxing/unboxing objects
if (value is int) __refvalue(__makeref(value), int) = 1;
else value = default(T); }
}
0
и насколько это улучшило производительность?
+4
Где-то на 10% уменьшилось количество блокировок. У нас в одном очень часто вызываемом месте стоял HasFlag().
+1
А где возникали блокировки? При выделении памяти в куче? Я не пишу под Mono, но на сколько помню, там для каждого потока в куче выделяется фрейм, и блокировка возникает только в том случае, если необходимо выделить память под новый объект, не помещающийся во фрейм. Опять же не уверен, но я думаю, что при вызове HasFlag на фрейме будет аллоцироваться один объект, и затем при выходе из метода он будет сразу удаляться (зависит от конкретной реализации VM, но в Mono, кажется, так и есть), таким образом объем фрейма будет постоянным. Или я не прав, и в Mono всё хуже? :)
+1
На моно есть boehm и sgen (может еще что-то есть, но я не знаю). Это два разных GC по идеологии. Мы пользуемся sgen (это generational GC), а то, что вы описали это boehm. Можно изучить вопрос, но даже если так, то в любом случае sgen себя показывает лучше чем boehm в наших условиях.
0
Кстати, фрейм под поток? Аллоцировать объект на фрейме и очищать после выхода из метода? Мне кажется вы описываете стук.
А под моно происходит вот что:
#define LOCK_GC do { mono_mutex_lock (&gc_mutex); MONO_GC_LOCKED (); } while (0)
LOCK_GC;
res = mono_gc_alloc_obj_nolock (vtable, size);
Но я почти уверен, что что-то подобное и в .NET происходит, потому что мы там тоже видим lock.
А под моно происходит вот что:
#define LOCK_GC do { mono_mutex_lock (&gc_mutex); MONO_GC_LOCKED (); } while (0)
LOCK_GC;
res = mono_gc_alloc_obj_nolock (vtable, size);
Но я почти уверен, что что-то подобное и в .NET происходит, потому что мы там тоже видим lock.
0
*стэк конечно
0
Нет, вот что я имел ввиду:
Использовал неправильный термин :)
Allocation
In a classic semi-space collector allocation is done in a linear fashion by pointer bumping, i.e. simply incrementing the pointer that indicates the start of the current semi-space’s region that is still unused by the amount of memory that is required for the new object.
In a multi-threaded environment like Mono using a single pointer would introduce additional costs because we would have to do at least a compare-and-swap to increment it, potentially looping because it might fail due to contention. The standard solution to this problem is to give each thread a little piece of the nursery exclusively from which it can bump-allocate without contention. Synchronization is only needed when a thread has filled up its piece and needs a new one, which should be far less frequent. These pieces of nursery are called “thread local allocation buffers”, or “TLABs” for short.
Использовал неправильный термин :)
0
При выходе из меотда сборок не происходит. И память очевидно не освобождается.И в .NET и в Mono.
Вы путаете аллокацию value и reference типов.
Вы путаете аллокацию value и reference типов.
0
блокировок чего? GC?
0
А профилировщиками или бенчмарками пункты 1-2-3 меряли?
Мне просто хочется посмотреть в своём проекте, насколько большой вклад дают эти вещи в тормоза в целом, и имеет ли смысл срочно ими заморочиться. Хорошо бы знать, как лишний боксинг будет выглядеть в профайлере, например встроенном или джетбрейновском.
Мне просто хочется посмотреть в своём проекте, насколько большой вклад дают эти вещи в тормоза в целом, и имеет ли смысл срочно ими заморочиться. Хорошо бы знать, как лишний боксинг будет выглядеть в профайлере, например встроенном или джетбрейновском.
+4
Профайлерам не нужно знать что такое боксинг.
Если пользуете R# — поставьте github.com/controlflow/resharper-heapview
Если пользуете R# — поставьте github.com/controlflow/resharper-heapview
0
Я раньше постоянно из эстетических соображений переписывал стандартные конструкции вида "(a & b) == b" на a.HasFlag(b). До тех пор, пока не запустил профайлер и не обнаружил миллионы ненужных боксов. Ясно, что это короткоживущие объекты, и они уничтожаются практически сразу, но всё-равно я не понимаю, почему разработчики CLR сделали такую кривую реализацию этого метода. Лучше бы вообще не делали.
+3
Здорово! Спасибо за статью. Очень был удивлен по поводу enum-ов. Все время старался использовать именно их в качестве ключа для Dictionary из предположения «легковесности» (по крайней мере мне так казалось) вычисления GetHashCode-а.
0
Отвечаю сразу на два вопроса по тому, насколько это поможет в ваших проектах. Сложно сказать, я же не видел ваш код. Вообще, как я написал вначале, проблемы производительности обычно лежат на уровень выше и надо просто запускать профайлер и решать одну проблему за другой. Насчет того, как найти в профайлере. В профайлере можно часто увидеть различные value типы в списках объектов, живущих на куче. Если их очень много, и они выходят на первые строки, то надо начинать задумываться. Мы смотрели блокировки и получили такой stack trace (но это я уже забегаю в тему следующей статьи):
stack
#4 0x00007ffeda83cc72 in __lll_lock_wait () from /lib/libpthread.so.0
#5 0x00007ffeda838179 in _L_lock_953 () from /lib/libpthread.so.0
#6 0x00007ffeda837f9b in pthread_mutex_lock () from /lib/libpthread.so.0
#7 0x00000000005f9269 in mono_gc_alloc_obj (vtable=vtable(«System.Int32»), size=20) at sgen-alloc.c:468
#8 0x00000000005b4b4d in mono_object_new_alloc_specific (vtable=vtable(0x2)) at object.c:4481
#9 0x00000000005b55e8 in mono_object_new_specific (vtable=vtable(«System.Int32»)) at object.c:4472
#10 0x00000000005379a9 in ves_icall_System_Enum_get_value (this=0x7ffed94ac8f0) at icall.c:3093
#11 0x00000000415197dd in (wrapper managed-to-native) System.Enum:get_value (param0=<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
140732543977712) at :668
#12 0x0000000041643994 in System.Enum:HasFlag (this=<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
140732543977712, flag=<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
140732543977736) at /root/mike/mono/mcs/class/corlib/System/Enum.cs:1991
#5 0x00007ffeda838179 in _L_lock_953 () from /lib/libpthread.so.0
#6 0x00007ffeda837f9b in pthread_mutex_lock () from /lib/libpthread.so.0
#7 0x00000000005f9269 in mono_gc_alloc_obj (vtable=vtable(«System.Int32»), size=20) at sgen-alloc.c:468
#8 0x00000000005b4b4d in mono_object_new_alloc_specific (vtable=vtable(0x2)) at object.c:4481
#9 0x00000000005b55e8 in mono_object_new_specific (vtable=vtable(«System.Int32»)) at object.c:4472
#10 0x00000000005379a9 in ves_icall_System_Enum_get_value (this=0x7ffed94ac8f0) at icall.c:3093
#11 0x00000000415197dd in (wrapper managed-to-native) System.Enum:get_value (param0=<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
140732543977712) at :668
#12 0x0000000041643994 in System.Enum:HasFlag (this=<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
140732543977712, flag=<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
<type 'exceptions.RuntimeError'>
Cannot access memory at address 0x190000000002b2
140732543977736) at /root/mike/mono/mcs/class/corlib/System/Enum.cs:1991
0
1. Первое правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
2. Второе правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
3. Третье правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
4. Если boxing/unboxing стал решающим, значит сервер и так уже супероптимизирован и форматирование строк, например, там вовсе не используется
2. Второе правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
3. Третье правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
4. Если boxing/unboxing стал решающим, значит сервер и так уже супероптимизирован и форматирование строк, например, там вовсе не используется
+2
Да, как я написал, форматирование строк у нас всплыло в основном в создании различных исключений, а в этом случае боксинг не самая большая ваша проблема. Перед тем, как дойти до оптимизирования боксинга мы не один месяц правили другие участки кода.
+1
Это стоит добавить в статью. Я пока не видел ситуацию чтобы что либо в ToString садило перфоманс. Если такое будет — таких программистов надо за кольцевую вывозить и долго бить. Если уже кто то яростно клеит строки и это доменная задача — там понятно что будут не ToString использовать.
А то сейчас начнется, в каждом проекте будет каша в ToString и ребята с круглыми глазенками будут доказывать что дядя на хабре писал что так медленнее.
Если для value типов делать ToString то и строки форматирования надо в каждый отдельный передавать, это немного убивает смысл ToString с единой строкой форматирования.
А то сейчас начнется, в каждом проекте будет каша в ToString и ребята с круглыми глазенками будут доказывать что дядя на хабре писал что так медленнее.
Если для value типов делать ToString то и строки форматирования надо в каждый отдельный передавать, это немного убивает смысл ToString с единой строкой форматирования.
0
Сразу сделаю ремарку: чаще всего проблемы производительности лежат на более высоком уровне, и прежде чем править весь лишний boxing, нужно привести код к такому состоянию, когда от этого будет толк.
Я же вначале написал, не надо кидаться в такие крайности, если у вас нет с этим проблем. Просто, если код писали не совсем криво, то мало будет мест, которые можно исправить и сразу +200% к производительности. Наступает момент, когда тут немного, там немного и получили +10%. Мелочь, а приятно.
+1
Утверждается, что боксинг и оператор as дороже, чем два вызова приведения типа: is + (T)?
0
Скажем так: в конкретно нашем случае, мы лучше сделаем несколько лишних операций, чем выделим лишнюю память, которая влияет на GC и тем самым может вызвать блокировку потока. То есть мы пока еще не грузим процессор в 100% всё время, потому что есть блокировки, немалая часть из которых вызваны из-за GC.
0
В WPF, который делает дофига боксов при доступе к свойствам, есть вот такая вещь:
Она internal, но можно добавить код в свой проект и тоже использовать.
public static class BooleanBoxes
{
public static readonly object True = true;
public static readonly object False = false;
[Pure]
public static object Box(bool value)
{
Contract.Ensures(Contract.Result<object>() != null);
return value ? True : False;
}
}
Она internal, но можно добавить код в свой проект и тоже использовать.
0
А как это помогает от боксов?
0
Позволяет в одной коллекции хранить объекты различного типа без боксинга. В WPF значения хранятся в EffectiveValueEntry, которые структуры, но значения внутри себя хранят в Object поле. Если количество различных типов заранее известно, то можно использовать union, чтобы не боксить, но при этом хранить их в одном месте.
0
При использовании ToString в сочетании с Format есть одна вещь, про которую легко забыть — если форматирование делается не с CurrentCulture (а, например, с InvariantCulture), то его надо передавать и в Format, и во все ToString. Хотя имхо вообще культуру лучше явно передавать всегда там, где есть такая возможность, и настроить варнинги соответствующим образом.
>> Если же метод работает и с value типами и с reference типами, то, например, сравнение на null лучше писать так
Сравнение с null для value-типов в языке определено отдельно, и не приводит к реальному боксингу. Интереса ради можете попробовать сравнить какой-нибудь Int32 с null в не-generic коде, и посмотреть на IL.
Вообще, тут главное помнить, что любой generic метод, если параметром-типом ему передать value-тип, получит отдельную реализацию на уровне JIT, где конкретный T известен — и оптимизатор там порезвится вволю. Из моих личных экспериментов, генерируемый ассемблерный код практически всегда эквивалентен тому, что получается при ручной подстановке.
>> оператор as работает только с reference типами
Это не так — он работает и с nullable value-типами. К вашему случаю это, правда, не относится поскольку нельзя написать T?, если нет struct constraint (кстати, это, наверное, наиболее косячная деталь в реализации nullable в CLR). Но поскольку у value-типа будет своя инстанциация дженерика, я почти стопроцентно уверен, что JIT-оптимизатор выкинет в ней и box, и isinst, и просто подставит там константу. Хотя это надо проверить.
>> Если же метод работает и с value типами и с reference типами, то, например, сравнение на null лучше писать так
Сравнение с null для value-типов в языке определено отдельно, и не приводит к реальному боксингу. Интереса ради можете попробовать сравнить какой-нибудь Int32 с null в не-generic коде, и посмотреть на IL.
Вообще, тут главное помнить, что любой generic метод, если параметром-типом ему передать value-тип, получит отдельную реализацию на уровне JIT, где конкретный T известен — и оптимизатор там порезвится вволю. Из моих личных экспериментов, генерируемый ассемблерный код практически всегда эквивалентен тому, что получается при ручной подстановке.
>> оператор as работает только с reference типами
Это не так — он работает и с nullable value-типами. К вашему случаю это, правда, не относится поскольку нельзя написать T?, если нет struct constraint (кстати, это, наверное, наиболее косячная деталь в реализации nullable в CLR). Но поскольку у value-типа будет своя инстанциация дженерика, я почти стопроцентно уверен, что JIT-оптимизатор выкинет в ней и box, и isinst, и просто подставит там константу. Хотя это надо проверить.
+2
То, что оптимизатор многие вещи убирает я уже понял, но так как я пишу под моно, то я предпочитаю некоторые вещи писать явно и не зависеть от реализации. А насчет nullable, так это вообще одно большое исключение из правил.
0
Зарегистрируйтесь на Хабре, чтобы оставить комментарий
Улучшаем производительность: boxing в .NET, которого можно избежать