Pull to refresh

Comments 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); }

}
Мне кажется в этом случае, когда известен тип и вы гонитесь за такой производительностью, то проще написать отдельный метод под Int32.
и насколько это улучшило производительность?
Где-то на 10% уменьшилось количество блокировок. У нас в одном очень часто вызываемом месте стоял HasFlag().
А где возникали блокировки? При выделении памяти в куче? Я не пишу под Mono, но на сколько помню, там для каждого потока в куче выделяется фрейм, и блокировка возникает только в том случае, если необходимо выделить память под новый объект, не помещающийся во фрейм. Опять же не уверен, но я думаю, что при вызове HasFlag на фрейме будет аллоцироваться один объект, и затем при выходе из метода он будет сразу удаляться (зависит от конкретной реализации VM, но в Mono, кажется, так и есть), таким образом объем фрейма будет постоянным. Или я не прав, и в Mono всё хуже? :)
На моно есть boehm и sgen (может еще что-то есть, но я не знаю). Это два разных GC по идеологии. Мы пользуемся sgen (это generational GC), а то, что вы описали это boehm. Можно изучить вопрос, но даже если так, то в любом случае sgen себя показывает лучше чем boehm в наших условиях.
Кстати, фрейм под поток? Аллоцировать объект на фрейме и очищать после выхода из метода? Мне кажется вы описываете стук.

А под моно происходит вот что:

#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.
Нет, вот что я имел ввиду:

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.


Использовал неправильный термин :)
Но да, похоже, что при выходе из метода память не очищается. Тогда понятно, откуда могут быть блокировки. :)
Да, всё так. Но как всегда теория выглядит хорошо, а в реальности не всё так безоблачно. Исходники открытые, можете почитать. Интересное чтиво :)
При выходе из меотда сборок не происходит. И память очевидно не освобождается.И в .NET и в Mono.
Вы путаете аллокацию value и reference типов.

Вообще, я имею в виду все блокировки, то есть когда мы сваливаемся в kernel lock и поток спит. Но в данном случае — да, проблема была в GC.
А профилировщиками или бенчмарками пункты 1-2-3 меряли?

Мне просто хочется посмотреть в своём проекте, насколько большой вклад дают эти вещи в тормоза в целом, и имеет ли смысл срочно ими заморочиться. Хорошо бы знать, как лишний боксинг будет выглядеть в профайлере, например встроенном или джетбрейновском.
Я раньше постоянно из эстетических соображений переписывал стандартные конструкции вида "(a & b) == b" на a.HasFlag(b). До тех пор, пока не запустил профайлер и не обнаружил миллионы ненужных боксов. Ясно, что это короткоживущие объекты, и они уничтожаются практически сразу, но всё-равно я не понимаю, почему разработчики CLR сделали такую кривую реализацию этого метода. Лучше бы вообще не делали.
Здорово! Спасибо за статью. Очень был удивлен по поводу enum-ов. Все время старался использовать именно их в качестве ключа для Dictionary из предположения «легковесности» (по крайней мере мне так казалось) вычисления GetHashCode-а.
Отвечаю сразу на два вопроса по тому, насколько это поможет в ваших проектах. Сложно сказать, я же не видел ваш код. Вообще, как я написал вначале, проблемы производительности обычно лежат на уровень выше и надо просто запускать профайлер и решать одну проблему за другой. Насчет того, как найти в профайлере. В профайлере можно часто увидеть различные 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
А каким профайлером пользуетесь, если не секрет?
RedGate, студийным, Concurrency Visualizer for Visual Studio, профайлером для mono, gdb. Еще пробовали Intel® VTune™ Amplifier XE 2013 — прикольная штука.
1. Первое правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
2. Второе правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
3. Третье правило оптимизации: Никогда не занимайся оптимизацией без профилирования!
4. Если boxing/unboxing стал решающим, значит сервер и так уже супероптимизирован и форматирование строк, например, там вовсе не используется
Да, как я написал, форматирование строк у нас всплыло в основном в создании различных исключений, а в этом случае боксинг не самая большая ваша проблема. Перед тем, как дойти до оптимизирования боксинга мы не один месяц правили другие участки кода.
Это стоит добавить в статью. Я пока не видел ситуацию чтобы что либо в ToString садило перфоманс. Если такое будет — таких программистов надо за кольцевую вывозить и долго бить. Если уже кто то яростно клеит строки и это доменная задача — там понятно что будут не ToString использовать.

А то сейчас начнется, в каждом проекте будет каша в ToString и ребята с круглыми глазенками будут доказывать что дядя на хабре писал что так медленнее.
Если для value типов делать ToString то и строки форматирования надо в каждый отдельный передавать, это немного убивает смысл ToString с единой строкой форматирования.
Сразу сделаю ремарку: чаще всего проблемы производительности лежат на более высоком уровне, и прежде чем править весь лишний boxing, нужно привести код к такому состоянию, когда от этого будет толк.


Я же вначале написал, не надо кидаться в такие крайности, если у вас нет с этим проблем. Просто, если код писали не совсем криво, то мало будет мест, которые можно исправить и сразу +200% к производительности. Наступает момент, когда тут немного, там немного и получили +10%. Мелочь, а приятно.
Утверждается, что боксинг и оператор as дороже, чем два вызова приведения типа: is + (T)?
Скажем так: в конкретно нашем случае, мы лучше сделаем несколько лишних операций, чем выделим лишнюю память, которая влияет на GC и тем самым может вызвать блокировку потока. То есть мы пока еще не грузим процессор в 100% всё время, потому что есть блокировки, немалая часть из которых вызваны из-за GC.
В WPF, который делает дофига боксов при доступе к свойствам, есть вот такая вещь:

    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, но можно добавить код в свой проект и тоже использовать.
Да, знаю. У нас тоже есть в некоторых местах такие штуки. А еще вот так извращаемся.
А как это помогает от боксов?
Позволяет в одной коллекции хранить объекты различного типа без боксинга. В WPF значения хранятся в EffectiveValueEntry, которые структуры, но значения внутри себя хранят в Object поле. Если количество различных типов заранее известно, то можно использовать union, чтобы не боксить, но при этом хранить их в одном месте.
При использовании 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, и просто подставит там константу. Хотя это надо проверить.
То, что оптимизатор многие вещи убирает я уже понял, но так как я пишу под моно, то я предпочитаю некоторые вещи писать явно и не зависеть от реализации. А насчет nullable, так это вообще одно большое исключение из правил.
Кстати, это была бы сама по себе интересная тема — сравнить выход JIT у Mono и .NET на таких вот моментах.
Да, интересная тема. Скорей всего займусь, тем более, что при обнаружении очевидных проблем, можно будет самому исправить в коде моно и попробовать влить в основной репозиторий.
Sign up to leave a comment.

Articles

Change theme settings