Как стать автором
Обновить

Комментарии 229

Dictionary<Type, RipeType> — непотокобезопасный контейнер. Нельзя его в одном потоке читать, а в другом в то же самое время писать! Да и завязываться на то что компилятор закеширует передаваемый в Lock.Invoke делегат тоже не стоит...


Почему бы не использовать ConcurrentDictionary вместо велосипеда?

Можно и ConcurrentDictionary использовать, если нужно.

По началу у меня самого были подозрения насчёт такого решения, но при более детальном анализе я пришёл к выводу, что оно довольно безопасное. Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке… Мне самому интересно о нём узнать, если он существует.
Да и завязываться на то что компилятор закеширует передаваемый в Lock.Invoke делегат тоже не стоит...

Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому lock сработает корректно.
Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке…

Пока один поток меняет словарь, второй из него читает и получает мусорные данные. Например, не до конца заполненный RipeType. Или падает с NPE из-за видимого нарушения внутренней структуры словаря.

Внешняя ссылка на экземпляр класса RipeType может появится только после выполнения конструктора, в каком бы потоке мы ни выполняли оператор new. Исключение составляет лишь случай вроде
    class AnyClass
    {
        public static AnyClass Instance;

        public AnyClass()
        {
            Instance = this;
            /* ... */
        }
    }

Но это не наша ситуация, поэтому вариант с недоинициализированным RipeType отпадает.

По логике вещей, при чтении структура словаря не может быть нарушена, даже если оно идёт из разных потоков. При параллельной записи тоже, поскольку есть lock. Остаётся лишь случай чтения в момент записи… Мне думается, что словарь не бросит исключение от такого, а если вдруг чтение произошло до момента вставки только что созданного экземпляра и вернулся null, то мы направляемся в lock и дожидаемся завершения вставки, после чего повторяем чтение и получаем уже созданный экземпляр.
Вы же понимаете, что чтение/запись в хэш-таблицу — это не атомарные операции?

З. Ы. Блокировка на делегате — это совсем жесть, конечно.
Понимаю, конечно. Но в данном решении атомарность и не требуется за счёт повторного чтения под локом.

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

То есть то, что у вас чтение бывает для разных ключей, вы и забыли, да?

Повторное чтение происходит по тому же ключу, что и при первой неудачной попытке.
Что произойдет при первом чтении по двум разным ключам «одновременно» из двух потоков?

Боттлнек на пустом месте, да.

… вот именно поэтому не надо придумывать свою реализацию, не разобравшись в проблеме.


Вот у вас есть словарь, в нем есть значение для ключа A. Теперь к вам одновременно пришли запросы для ключей A и B. Первый попадет в чтение, второй — в запись, и они могут идти строго одновременно, потому что на первый не распространяется лок.

Не, не должно. У вас каждый раз при входе в метод будет создаваться новый экземпляр скрытого типа, и поэтому lock всегда будет получать новый объект.

Мне думается, что словарь не бросит исключение от такого

Не, не бросит. Просто тихо вернет не то значение. Вы внутрь TryGetValue никогда не заглядывали?

Заглядывал когда-то давно. Конечно, могу ошибаться, но на вскидку такое маловероятно, поскольку если не найдено соответствия по ключу, то с чего бы возвращать дугое значение. Теоретически, может быть такое, что ключ уже попал в словарь, а значение пока ещё не присвоилось, но тогда хотя бы дефолтный null вернуться должен.

Я прямо говорю, что мной выбрано такое решение в целях эксперимента и обсуждения, поскольку есть подозрение, что оно рабочее.
Там внутри таблица связных списков поверх массива с ресайзом этого самого массива, в ходе которого элементы могут быть заново переразложены по спискам с пересчетом хэшкодов со случайной солью. То есть, никто не гарантирует, что при пересчете элементы хэш-таблицы «случайно» не обменяются хэшкодами.

UPD github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/mscorlib/system/collections/generic/dictionary.cs#L386
Обменяться-то они могут, но по хэш-коду вычисляется номер «корзины» (связного списка), а поиск в списке уже идёт по строгой эквивалентности ключа, поэтому в худшем случае элемент может не найтись, хотя он в словаре присутствует (и-то мне видится это крайне маловероятным событием, если вообще возможным).
Только вот корзина — это связный список на индексах. И все корзины лежат в одном массиве, а индексы у элементов меняются, причем перемещение элемента неатомарное и включает, кажется, с полдесятка операций записи и зависит не то чтобы от конкретного рантайма, а от конкретной его сборки.

Построить полную картину того, что может произойти при контеншне не берусь, там сотни вариантов с разным исходом, в том числе возврате любого произвольного элемента вместо null.

Самый простой вам ниже lair показал.

Я например видел такое маловероятное событие на проде два раза. Один раз ночью в выходной

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

А вы загляните.


int entry = this.FindEntry(key);
if (entry >= 0)
{
  value = this.entries[entry].value;
  return true;
}

Вот если после FindEntry массив entries поменяет размер (с перекладкой всего), а именно это происходит (иногда) при добавлении новой записи, значение по индексу (entries[entry]) будет совсем не от нужной записи.


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

Неправильное подозрение. Dictionary<K,V> нельзя использовать в сценариях конкурентного чтения/записи без явной блокировки всех операций.

поменяет размер (с перекладкой всего)

… на самом деле, немного сложнее, потому что entries в той реализации, на которую я смотрю, никогда не перемешивается. Зато внутри FindEntry есть прекрасный код, смотрит на this.buckets[num % this.buckets.Length] и в зависимости от настроения оптимизатора в этом месте можно получить что угодно, включая погоду на Марсе, например, когда между обращением к buckets.Length и обращением к buckets[x] содержимое buckets поменялось — а вот buckets точно перемешивается.

Это, конечно, интересный момент, но по беглому изучению кода выглядит так, что entries при добавлении новых элементов может лишь увеличиваться в размере, а перекладка элементов в новый массив происходит без смешивания, через Array.Copy, поэтому даже старый индекс будет валиден в случае нового массива, вопрос остаётся открытым…
Выглядит так, что в худшем случае мы можем лишь потерять элемент, уже находящийся в словаре, что приведёт к его пересозданию извне, но если элемент по ключу найден, то чтение безопасно, поскольку индеск в массиве за ним закрепляется навсегда.
Выглядит так, что в худшем случае мы можем лишь потерять элемент, уже находящийся в словаре, что приведёт к его пересозданию извне

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

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

Вы проверил все возможные сценарии, со всеми расположениями переменных? Например, что случится, если у вас происходит два одновременных добавления, и два потока одновременно заберут ссылку на следующий свободный элемент, и там окажется хэш-код и ключ от одного элемента, а значение — от другого?

Нет, я не проверял все возможные сценарии и отталкиваюсь лишь от того, что TryGetValue возвращает true и сам элемент, если он присутствует в словаре по ключу, либо false если его там нет или он только что асинхронно добавлен в процессе чтения другим потоком (и на этот случай выполняется повторное чтение в критической секции).

Вы, повторюсь, забыли, что у вас параллельно еще присвоения идут?

Запись элементов идёт только под lock'ом. То есть я допускаю лишь ситуацию с ненадёжным параллельным чтением, которая обрабатывается под тем же lock'ом.
Запись элементов идёт только под lock'ом.

… который у вас долгое время не работал. Кстати, про syncroot на коллекциях вы не знаете, да?


То есть я допускаю лишь ситуацию с ненадёжным параллельным чтением, которая обрабатывается под тем же lock'ом.

Потенциально возвращенный null вы тоже обрабатываете? Что-то не видно.

Кстати, про syncroot на коллекциях вы не знаете, да?

Знаю, но в некоторых случаях мне хотелось бы абстрагироваться от введения явной переменной, отчего и появился
public static class Lock
{
	public static TResult Invoke<TSyncContext, TResult>(Func<TResult> func)
	{
		lock (func) return func();
	}
}

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

Потенциально возвращенный null вы тоже обрабатываете? Что-то не видно.

Обрабатывается потенциально возвращённый false. :)
Знаю, но в некоторых случаях мне хотелось бы абстрагироваться от введения явной переменной

… поэтому вы ввели дополнительное поле вместо использование существующего.


Кстати, ваш словарь еще и публичный, поэтому все и любые утверждения про то, что модификация только под локом, невалидны.


Обрабатывается потенциально возвращённый false

Вот только вы можете получить true и null.

Кстати, ваш словарь еще и публичный, поэтому все и любые утверждения про то, что модификация только под локом, невалидны.

Спасибо, в коде я это уже подправил, а в статье осталась неточность, подкорректирую.

Вот только вы можете получить true и null.
Может быть, и могу. Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам. :)

Конечно, в коммерческих рабочих проектах я обычно применяю более надёжные решения, но в своих исследовательских ни в чём таком себя не сдерживаю.
Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам.

… а потом доказывать, что они безопасные. Спасибо, но нет.


(и нет, вы ничего не испытываете, потому что вы не видите никаких проблем в вашем коде, пока вам на них не покажут)

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

Во-первых, даже опытные разработчики могут не увидеть проблемы в своём коде, которая выльется в виде бага.

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

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

Как мы уже неоднократно наблюдали (и обсуждали), у вас очень удобное для вас понимание убедительных аргументов.


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

Что-то этой проверки не видно в посте.


Для меня подобные публикации что-то вроде код-ревью от сообщества, с чем-то соглашаюсь, с чем-то нет.

Я и говорю: вы выбираете то, что вам хочется. Правильность или ее отсутствие вас не волнуют.

Что-то этой проверки не видно в посте.
Вы же мне помогали в комментариях совместно с другими людьми проверять некоторые мои допущения о словаре и блокировках. Основа поста — идеи для реализации TypeOf и RipeType, а моя имплементация вполне может иметь недостатки, и я очень рад, что мне на них указали.

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

… если бы вы были готовы признавать свои ошибки, в вашем коде уже давно был бы ConcurrentDictionary или ImmutableDictionary. Но вы продолжаете костылить вокруг обычного.

Дело в том, что часть тестов производительности проведена на обычном словаре, поэтому я не хочу сейчас менять имплементацию и, соответственно, результаты тестов.

Всё предоставлено как есть, каждый сам может внести требуемые правки в реализацию при необходимости и проверить производительность.
Дело в том, что часть тестов производительности проведена на обычном словаре,

Который не подходит для этой задачи, и, значит, ваши тесты невалидны.

С чего вы взяли? Может, у кого-то однопоточное приложение и ему вполне хватит такого словаря.

Может, у кого-то приложение, в котором нет обращения к GetType в цикле, и ему не нужна мемоизация.


Вы сравниваете решение, которое корректно работает всегда, с решением, которое работает иногда, и никак это не оговариваете. Некрасиво.

Вы сравниваете решение, которое корректно работает всегда, с решением, которое работает иногда, и никак это не оговариваете. Некрасиво.
Данная некорректность обнаружилась уже в ходе обсуждения, и я сразу сделал честное примечание в тексте статьи, что лучше использовать более надёжное решение в плане потокобезопасности.

Чтобы уж не было никаких сомнений, укажу, что это может повлиять на производительность.
Данная некорректность обнаружилась уже в ходе обсуждения

Это как раз к разговору о ваших гипотезах. Вы даже не тестировали производительность в многопоточных режимах.


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

И какой тогда смысл в проведенных вами тестах, если половину кода под ними использовать нельзя?

А другую половину можно.

Угу, и какая есть какая, надо догадываться самостоятельно.


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

Скажу прямо — если бы кто-то мне платил за то время и силы, что я трачу на статьи, то можно было бы говорить о разжёвывании материала, подробном анализе и детальном рассмотрении всех возможных аспектов.

Сейчас я делаю это as is — указываю на ключевые моменты и идеи, предоставляю примеры, а читатель уже сам решит, что и как ему с этим делать. Я ничего не продаю и не рекламирую, если и публикую ссылки на код, то лишь делюсь личными наработками с другими людьми, и да, иногда кто-то находит там для себя что-то интересное.
Скажу прямо — если бы кто-то мне платил за то время и силы, что я трачу на статьи, то можно было бы говорить о разжёвывании материала, подробном анализе и детальном рассмотрении всех возможных аспектов.

Это, конечно, повод, когда вам не платят за ваше время и силы, публиковать посредственные статьи с плохим анализом.


лишь делюсь личными наработками с другими людьми

… и о качестве этих наработок, я полагаю из сказанного выше, можно сказать то же самое, что и о качестве этой статьи.

В этой статье я не занимаюсь анализом, а всего лишь, как сказано ранее, выдвигаю гипотезу, провожу эксперименты и делюсь результатами — в этом моя цель.

И одна из причин, почему не углубляюсь в анализ, это те дикие дебри, которые лежат за полученными данными, почему они именно такие…

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

Какой смысл в результатах (неправильно проведенного эксперимента) без анализа?


Кстати, а как же вы делаете выводы (которые в вашей статье есть) без анализа? Просто "что придумывалось"?


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

Ну то есть вы вывалили нам какие-то результаты, полученные неизвестно из чего, и даже не знаете, что они означают. Круто.

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

Кстати, а как же вы делаете выводы (которые в вашей статье есть) без анализа? Просто «что придумывалось»?
Выводы не выходят за рамки ответа на ваш предыдущий вопрос «в чём смысл?».

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

Вы под "способами ускорения вызовов" понимаете "давайте запишем в поле"? Серьезно?


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


Выводы не выходят за рамки ответа на ваш предыдущий вопрос «в чём смысл?».

Вы их сделали, не проводя анализа?


Раз вы так уверены в моём невежестве, то могли бы сами и пояснить, что они означают…

В том-то и дело, что они ничего не означают.

Серьёзно. Это просто, но не очевидно.

Замеры производительности на различных бенчмарках о многом говорят. И если вы считаете, что это рандомные значения, не несущие за собой смысла, то, пожалуйста, факты в студию, разрушьте мою иллюзию и раскроте глаза тем, кто в неё тоже начал верить.
Серьёзно. Это просто, но не очевидно.

Если вам это не было очевидно, то мне очень вас жаль. Для меня способ "оптимизации множественных вызовов" путем записи результата первого из них в переменную известен лет двадцать с лишним.


Замеры производительности на различных бенчмарках о многом говорят.

… и о чем же?


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

Вы про чайник Рассела никогда не слышали?

Извините, конечно, но в дженерик случае через статических класс — это не настолько очевидно, как в обычном. Вы сами-то применяли осознанно такое решение раньше? А если применяли, то на каком году программирования дошли?

… и о чем же?
Оставляю на ваш суд.

Про чайник теперь услышал.
Вы сами-то применяли осознанно такое решение раньше?

Ну да.


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

На первом году пользования дженериками, ровно в тот момент, когда понял, что для каждого варианта дженерика в .net создается свой тип.


Оставляю на ваш суд.

Смешно, да. Когда я вам говорю, что замеры бессмысленны, вы просите это доказать фактами. А когда я спрашиваю, что они значат, вы оставляете это на мой суд.

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

class Cache<T>
{
    public object Instance { get; }
}

static void AnyMethod<T>()
{
    var anyInstance = Cache<T>.Value ?? 
    Cache<T>.Value = ReadOrCreate<T>();
}

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

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

… а это удобно? Никогда бы не подумал. И код, который вы приводите, традиционно плох. Даже нет, не плох — ужасен.


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

В таком случае заодно можно считать, что этим статьям не место на хабре, потому что мне хочется думать, что аудитория здесь не состоит из "таких же тугодумов".

… а это удобно? Никогда бы не подумал. И код, который вы приводите, традиционно плох. Даже нет, не плох — ужасен.
Да? Тогда задачка для вас: закэшируйте значение в статическом дженерик методе AnyPerformanceCriticalMethod⟨T⟩(), зависящее от параметра T, это же просто, верно? Мне очень интересно увидеть ваше более оптимальное по производительности решение, потому что лучшего я не знаю, а хуже да, могу предложить.

В таком случае заодно можно считать, что этим статьям не место на хабре, потому что мне хочется думать, что аудитория здесь не состоит из «таких же тугодумов».
Вам, может, и хочется так думать, но своих читателей на Хабре публикации находят.
Тогда задачка для вас: закэшируйте значение в статическом дженерик методе AnyPerformanceCriticalMethod⟨T⟩(), зависящее от параметра T, это же просто, верно?

Зачем? Это единственно верная формулировка задачи?


Мне очень интересно увидеть ваше более оптимальное по производительности решение

Определите критерии оптимальности "производительности". Оптимальное по времени выполнения? По памяти? По одному с ограничением по другому? Без ограничений? В однопоточном сценарии? Многопоточном? Сколько разных T мы ожидаем? Какого размера кэшируемое значение? Какова стоимость создания значения?

Зачем? Это единственно верная формулировка задачи?
В статье и примерах кода, которые вы так дерзко критикуете, решается по сути именно такая задача — закэшировать данные, зависящие от дженерик параметра, для последующего максимально быстрого доступа. Вы утверждаете, что код ужасен, тогда предложите правильное решение…

Критерии:
— минимальное время выполнения при многократных вызовах
— серьёзных ограничений по памяти нет, потребление в переделах разумного
— чтение многопоточное
— достаточно одного T
— кэшируемое значение любое (для простоты bool, string, object)
— стоимость создания определяется так: если кэшированный доступ даёт выигрыш по производительности в 2 и более раза в сравнении с созданием, то задача решена
— желательно ещё, чтобы это было справедливо для любой CLR (.NET Framework, .NET Core, Mono).
В статье и примерах кода, которые вы так дерзко критикуете, решается по сути именно такая задача — закэшировать данные, зависящие от дженерик параметра, для последующего максимально быстрого доступа.

Так кто вам сказал, что это правильная задача?


  • минимальное время выполнения при многократных вызовах
  • достаточно одного T
  • чтение многопоточное


private static readonly bool _value = ValueGetter();

static void AnyMethod<T>()
{
    ... = _value;
}

  • серьёзных ограничений по памяти нет, потребление в переделах разумного

У всех разное понимание "разумного".


  • стоимость создания определяется так: если кэшированный доступ даёт выигрыш по производительности в 2 и более раза в сравнении с созданием, то задача решена

Вы не поняли вопроса. Чтобы знать, дает ли кэшированный доступ выигрыш, нужно знать три вещи: стоимость доступа к кэшу, количество обращений и стоимость создания значения. Первое я могу померять. Второе и третье — условия задачи.

private static readonly bool _value = ValueGetter();

static void AnyMethod<T>()
{
    ... = _value;
}

Возможно, я двусмысленно уточнил, но _value должно быть не общим значеним для любых T, а для каждого T конкретным. Под «достаточно одного T» имелось в виду, что у метода один дженерик параметр, а различных значений T пусть будет от 10 до 100.

Количество обращений более 100. Стоимость значения, как у typeof(T).Name/Assembly/IsValueType.
private static readonly ConcurrentDictionary<Type,bool> _values = new ConcurrentDictionary<Type,bool>();

static void AnyMethod<T>()
{
    ... = _values.GetOrAdd(typeof(T), ValueGetter);
}
Вот только замеров не видно и сравнения со статической версией.

А смысл что-то мерять? Это код, который выполняет поставленную задачу оптимальным с точки зрения написания кода образом. Я, напомню, говорил о качестве, а не о производительности вашего кода.


Если у написанного мной кода обнаружатся проблемы производительности, я буду их разбирать с профайлером и смотреть на профайлер. Абстрактные задачи "сделайте так, чтобы этот метод выполнялся в два раза быстрее" решать очень вредно, потому что внезапно может выясниться, что самое дешевое решение — закэшировать нужное значение снаружи метода.


(Все это, кстати, еще и потому, что написать хороший тест на производительно в условиях многопоточности — то еще искусство)

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

И насчёт «ужасного кода» вот яркий пример
EqualityComparer<T>.Default.Equals(a, b);
Самое дешёвое и универсальное решение [...] это замена этой конструкции на статическую версию TypeOf

Не доказано. Хотя бы потому, что не определена метрика "дешевизны".


Я вам больше того скажу, "решение" — оно всегда для проблемы, никогда не само по себе. А проблема пока не найдена.

Кстати, в обсуждаемом выше примере (AnyMethod<T>) и рефлексии-то, по сути, нет.

Рефлексия — частный случай более общего сценария с оптимиацией.

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

но ведь здесь typeof(T) будет вызываться каждый раз при вызове AnyMethod. А, если я не ошибаюсь, смысл в том, чтобы он вызывался один раз.
А, если я не ошибаюсь, смысл в том, чтобы он вызывался один раз.

Это неправильная постановка задачи. На двух платформах из трех вызов typeof(T) быстрее предлагаемого решения со статическим классом, а на третьей слишком большая ошибка измерения, чтобы был смысл об этом говорить.

У меня Тест на clr показал ошибку гораздо ниже, чем в статье. И была она ниже значения.

Повторюсь, это неправильная постановка задачи. Чтобы требовать уменьшения числа вызовов GetType() нужно доказать, что именно они вносят подавляющую долю в наблюдаемую проблему производительности.

Так это со всем так.
Под «достаточно одного T» имелось в виду, что у метода один дженерик параметр, а различных значений T пусть будет от 10 до 100.

В этот момент я заявляю, что вы страдаете херней и если у вас реально боттлнек в этом месте, то любой «паттерн» проиграет предзаполненному lookup table на Dictionary без блокировок вообще.
Вот вы собственноручно сравните скорость доступа к закэшированным данным в словаре (даже минимально заполненом, 1- 5 записей) и в случае статического дженерик класса с рид-онли переменной, а потом делайте вывод, занимаюсь я ерундой или ещё чем.

И заодно подумайте, почему
EqualityComparer<T>.Default.Equals(a, b);

реализован таким «ужасным» паттерном, а не хотя бы
EqualityComparer.GetDefault<T>().Equals(a, b);

… а что, бишь, ужасного-то в EqualityComparer<T>.Default?

Так а что ужасного в TypeOf⟨T⟩.GetSomething()?

Не нравится TypeOf⟨T⟩.GetSomething(), используйте эквивалентную форму TypeOf⟨T⟩.Ripe.GetSomething(), что аналогично EqualityComparer⟨T⟩.Default.Equals(a, b)
Так а что ужасного в TypeOf⟨T⟩.GetSomething()?

Во-первых, я про него и не писал ничего.
Во-вторых, вы знаете, чем отличается EqualityComparer<T> от вашего TypeOf<T>?


что аналогично EqualityComparer⟨T⟩.Default.Equals(a, b)

Нет, не аналогично.

В этой дискуссии, я писал про более общий паттерн кэширования при помощи статического дженерик класса, где частными случаями являются TypeOf⟨T⟩ и EqualityComparer⟨T⟩. Насколько понимаю, вы имели ввиду под «ужасным» кодом именно этот общий случай, который я схематически обозначил, и свою критику вы не детализировали.

Мне очень жаль, что вы не видете аналогии и не можете проследить общий паттерн.
В этой дискуссии, я писал про более общий паттерн кэширования при помощи статического дженерик класса, где частными случаями являются TypeOf⟨T⟩ и EqualityComparer⟨T⟩.

Вот только EqualityComparer<T> — не статический класс, и его (основной) задачей не является кэширование. Поэтому он не может быть частным случаем вашего паттерна.


Насколько понимаю, вы имели ввиду под «ужасным» кодом именно этот общий случай

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

Можете сделать RipeType дженериком (хотя это избыточно для конкретной задачи), а TypeOf⟨T⟩ переименовать в RipeType⟨T⟩ — получится та же картина, что и с EqualityComparer⟨T⟩.

Нет, не получится. Задача EqualityComparer<T> — совсем не в кэшировании.

Если бы совсем не в кэшировании и производительности, то можно было бы просто взять и на основе словаря, как у вас, сделать
EqualityComparer.GetDefault<T>()
.

Нельзя: класса EqualityComparer не существует.

Так в чём проблема сделать
EqualityComparerProvider.GetDefault<T>()

со словарём внутри, который предоставляет инстенсы EqualityComparer⟨T⟩? Почему выбрано иное решение?

Потому что это лишняя сущность. EqualityComparer<T> уже есть, и добавить в нем статическое поле — минимальный накладной расход (хотя я бы еще и Lazy его сделал, потому что бывает, что пишешь кастомный компарер, и дефолтный вызываться не будет никогда).

Эк вы резко перепрыгнули с кэширования результата вычисления метода к обычному в общем-то синглтону на ридонли статик поле.

Я вот вообще-то думал, что мы задачу про мемоизацию функции решаем.

UPD Я вот считаю, что кэш без вытеснения — и не кэш вовсе. Поэтому о задаче кэширования в вашем случае вообще можно не говорить.
Я вот вообще-то думал, что мы задачу про мемоизацию функции решаем.

Так и я же говорю, что в определённых дженерик случаях она прекрасно решается с высокой производительностью стататическим дженерик классом с рид-онли полем.

… но нет никакого повода считать это решение "хорошим". Хотя бы потому, что вы немножко замучаетесь генерить по статическому классу на каждую функцию.

Как я написал выше, оно хорошее в определённых дженерик случаях, где требуется высокая производительность.

"определенные дженерик случаи" — это очень смешно. Так определенные или дженерик? И если "там" требуется "высокая производительность", то лучший ли это способ ее достигнуть?


Повторюсь, оптимизационная задача "сделайте метод быстрее" неверна в корне. Смотреть надо на операцию (в прикладных терминах), которая происходит медленно, и разбираться, как ее оптимизировать. И ценой чего ее оптимизировать.

Как вам объяснить… Где я написал, что так нужно делать везде и всегда? Если вас устраивает вариант со словарём, то, пожалуйста, пользуйтесь!

В своей практике я дошёл до того момента, когда мне потребоволась кэшировать информацию о типах, чтобы достичь лушей производительности, и TypeOf с RipeType мне в этом помогли.

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

У меня нет цели доказывать кому-то что-то или навязывать. Нравится решение — бери и используй, не нравится — пробуй другое.
Где я написал, что так нужно делать везде и всегда?

Вот здесь:


Самое дешёвое и универсальное решение [...] в случае использования множественных рефлексивных вызовов на основе typeof в различных частях приложения — это замена этой конструкции на статическую версию TypeOf

[...]


В своей практике я дошёл до того момента,

Давайте определимся: в рабочей практике или в каких-то вольных исследовательских проектах? Иными словами, код, который вы нам показываете — он production ready или нет?


мне потребоволась кэшировать информацию о типах, чтобы достичь лушей производительности, и TypeOf с RipeType мне в этом помогли.

Теперь осталось выяснить, не было ли лучшего способа решить вашу проблему.


разбираться с деталями.

Мы же уже выяснили, что вы не делали анализа своих находок?

Что касается typeof, то в своих вольных проектах я везде заменил его на TypeOf, потому что для меня это самое оптимальное и универсальное решение для улучшения производительности.

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

Теперь осталось выяснить, не было ли лучшего способа решить вашу проблему.
К сожалению, способа получения информацию о типе минуя typeof или GetType я не знаю, но знаю, как её закэшировать для наиболее быстрого доступа в дальнейшем.

Мы же уже выяснили, что вы не делали анализа своих находок?
По моим личным критерия анализа, решения мне подходят. Вы свои критерии знаете куда лучше, поэтому применимость находок для себя сможете определить сами.
Что касается typeof, то в своих вольных проектах я везде заменил его на TypeOf

Вот именно слово "везде" и говорит о том, что вы не делали анализа, а просто что-то себе придумали и используете. Хотя нет (ну или я не знаю) ни одной оптимизации, которая была бы универсальна.


(в частности, в дженерик-типах ваше решение избыточно, но вы об этом не задумались)


что для меня это самое оптимальное и универсальное решение

… по удобным вам критериям, или, проще говоря, "я так решил".


К сожалению, способа получения информацию о типе минуя typeof или GetType я не знаю, но знаю, как её закэшировать для наиболее быстрого доступа в дальнейшем.

Вот только способов "кэширования" намного больше, чем вы тут рассмотрели.


По моим личным критерия анализа, решения мне подходят.

Ваши личные критерии мы уже как-то обсуждали.

Иногда по определённым вопросам спор с вами напоминает мне парадокс Кэррола.


Перед вами очевидные суждения:
А. чем меньше времени занимает событие, тем оно быстрее происходит
Б. определённые вызовы сравниваемых методов занимают меньше времени, чем другие


В. значит, эти вызовы быстрее


Вы же мне предлагаете доказывать справедливость В через кучу других суждений, уводящих в сторону.

Да нет, справедливость В (в его точной формулировки: "конкретные вызовы быстрее в конкретной ситуации") доказывает бенчмарк. Проблема не в этом утверждении, проблема в том, что вы заменили все вызовы typeof на ваш TypeOf, не рассмотрев другие варианты локальных оптимизаций, которые могли бы дать еще больший выигрыш (или быть оптимальным по другим критериям). А есть еще не-локальные оптимизации.


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

Варианты оптимизаций я рассматриваю для каждого случая индивидуально. Касательно typeof у меня был ряд сценариев, где его избежать нельзя. Судя по результатам бенчмарков в терминах скорости выполнения, использование TypeOf в большинстве случаев даёт ощутимый выигрыш.


Есть потенциальный минус в виде статических объектов, постоянно находящихся в памяти, однако для моих приложений это допустимо.

Варианты оптимизаций я рассматриваю для каждого случая индивидуально. Касательно typeof у меня был ряд сценариев, где его избежать нельзя.

И все равно "везде заменил"?


Судя по результатам бенчмарков в терминах скорости выполнения, использование TypeOf в большинстве случаев даёт ощутимый выигрыш.

Подозреваю, что это зависит от сценария использования.

Да, везде заменил, потому что медленнее работать не будет в моих случаях, плюс общность появляется, а не где-то typeof, а где-то TypeOf.


А плата в виде слегка повышенное потребления памяти вполне для меня допустима.

Да, везде заменил, потому что медленнее работать не будет в моих случаях, плюс общность появляется, а не где-то typeof, а где-то TypeOf.

Круто получается. "Был ряд сценариев, где избежать нельзя", но "везде заменил".


А уж про "медленеее работать не будет в моих случаях" мне и вовсе сказать нечего — мы про ваши случаи ничего не знаем, но судя по вашим примерам кода, от моих случаев они очень далеки.

Все мои случаи доступны в открытых репозиториях с кодом — изучайте при желании.

Это, простите, вот это? Без единого описания?


Спасибо, но нет.

Да. А зачем вам описания?


  1. Clone All
  2. Find All по TypeOf

Конечно, зесь вам самим решать, тратить на это время или нет.

А зачем вам описания?

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

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

Ну я и говорю: от моих случаев это бесконечно далеко.

Чтобы уж точно не было разночтений по стоимости создания, можете просто взять за эталон работу с типами из публикации typeof(T).Name/Assembly/IsValueType.

Нужно сделать быстрее в два раза при множественных вызовах.
На первом году пользования дженериками, ровно в тот момент, когда понял, что для каждого варианта дженерика в .net создается свой тип.

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

С практической точки зрения — всегда. Когда у вас есть


class Some<T>
{
  static Blah _foo;
}

вы можете быть уверенными, что значение _foo будет "разделено" между всеми экземплярами с одним и тем же T, и изолировано для разных T.

просто я думал, что они будут связаны через один MethodTable и просто добавляться в список статических полей. типа структура будет одна и та же, но для каждого типа будет создана своя статическая переменная в этом списке.
может поэтому кстати и так сильно отличаются результаты на core и clr. там же на core переписывали практически все, так что возможно, что и устройство типов(этих самых структур) и их принципы работы тоже изменились

Вот здесь мне сложно говорить, я не вдавался в эти детали (еще и потому, что для моих целей они не критичны). Описанное выше поведение — оно изначально не очень интуитивно, но к нему привыкаешь, и есть много мест, где им пользуются.

Может быть, и могу. Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам. :)


Меня в институте учили, что испытание/эксперимент, это:
1. Подробное изучение всей доступной информации об объекте эксперимента.
2. Выдвижение четкой гипотезы, базирующейся на известной информации, а не на предположениях.
3. Разработка и проведение повторяемых экспериментов, в том числе опровергающих гипотезу.
Я от этого далеко и не отхожу:

1. изучена работа typeof и Type
2. выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях
3. разработан и проведён ряд повторяемых экспериментов, в том числе опровергающих гипотезу

Получены результаты и предоставлены на рассмотрение широкому сообществу. :)
выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях

"… некоторых сценариях". Очень "четкая" гипотеза.

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

Но вы так и не определили, почему оно имеет разную производительность на разных фреймворках.

(Это я даже не начал вдаваться в статистический инструментарий под формулировкой гипотезы и ее проверкой)

Вообще-то определил, но, к сожалению, теперь вы не сможете просмотреть детали в открытом доступе, которые раньше находились тут.

В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов (некоторые добавляют дополнительную проверку на то, проинициализировано ли поле, что сказывается на производительности). Тема также тесно связана с добавлением статических конструкторов, у которых, как оказывается, есть ряд подводных камней…

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

Ну да, а публикации на хабре они недостойны, дадада.


В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов

И снова вопрос: почему?


Здесь вообще разговор для отельной статьи, так что при желании можете сами углубиться в этот вопрос

Мне-то это зачем? Это вы выдвигаете какие-то гипотезы, а у меня проблемы с производительностью совсем в других местах.

Ну да, а публикации на хабре они недостойны, дадада.
Достойны, но вы ж должны понимать, что статьи я пишу лишь в своё свободное время for free. Подготовка материалов даже к этой публикации потребовала уйму времени и сил. Да и пишу в основном о том, с чем сам сталкиваюсь на практике. Нужно или не нужно, люди сами уже для себя решат.

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

Печально.

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

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

Там есть дофига мест, где используется модуль от текущего (меняющегося) размера, так что можно получить много боли. Получить не тот айтем возвращенным так просто не выйдет, но вот нарваться на IndexOutOfRange или Duplicate Key — да.

По сути TryGetValue гарантирует, что не будет исключений из какого бы мы потока не работали со словарём. Может только false вернуться при несинхронном добавлении элемента из другого потока (поскольку словарь непотокобезопасный). Этот второй случай обрабатывается повторным чтением в lock.

Мне так видится реализация.
По сути TryGetValue гарантирует, что не будет исключений из какого бы мы потока не работали со словарём.

Не гарантирует. В текущей реализации мы не нашли места, где может быть исключение — это да. Но никаких гарантий нет.


Но у вас же и от одновременных присвоений нет никакой защиты.

Сама идеология работы метода при правильной имплементации должна гарантировать отсутствие всяких исключений. Если исключения есть, значит, плохо реализован метод, в нём баг.
При правильной имплементации и правильном использовании. Но вы используете его неправильно.
Пока убедительных аргументов, почему метод используется неправильно, я не услышал. Исключений нет, как выяснили, другой элемент тоже не придёт в результате. Если даже элемен вдруг потеряется, что крайне маловероятно, то в нашем случае ничего серьёзного не произойдёт, создадим новый вместо прежнего.
Возможно, для других сценариев это критично, что накладывает ограничения на применение, но для конкретного допустимо.
Почему для вас «Dictionary — не потокобезопасный класс» не аргумент?
Можно два определения дать потокобезопасности:
1. Гаранития того, что коллекция вообще будет работать в условиях нескольких потоков
2. Гарантия того, что при записи/удалении/замене элемента одним потоком, второй изменения сразу же увидит

Сейчас я придерживаюсь второго, более сильного. Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.
От смены определения Dictionary в условиях нескольких потоков гарантированно работать не начнет.
Изначально я придерживаюсь такого определения:
Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.

Таких гарантий по отношению к Dictionary никто не дает (ну, если, конечно, вы не считаете периодическое бросание исключений "ненадежным результатом").

И попробуйте использовать это, скажем, в платежной системе.
Зачисляете Вы деньги на свой счет периодически, а может иногда зачислять Ваши деньги на чужой счет. Это хорошо?

Для таких случаев есть служба поддержки в платёжной системе.


(: Шутка!

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

Это только в текущей реализации. Завтра ее поменяют — и у вас все упадет (это, если что, говорит человек, у которого именно такое случилось при апгрейде с 4.7.1 на 4.7.2).

Любопытно, с чем именно вы столкнулись при апгрейде? Мне просто интересно узнавать такие тонкости в реализациях стандартных классов.

С изменением внутренней реализации EtwTrace для asp.net.

Вы забыли одно важное дополнение: при выполнении предусловий.


Теперь открываем документацию:


A Dictionary<TKey, TValue> can support multiple readers concurrently, as long as the collection is not modified.

Выделенное мное условие у вас не выполняется. Я вам больше того скажу, в общем случае TryGetValue может упасть и сейчас — если параллельно Clear вызвать.

Не вижу причин для падения даже при параллельном Clear.

Плохо смотрите.


1:


for (int i = 0; i < buckets.Length; i++) buckets[i] = -1;

2:


int i = buckets[hashCode % buckets.Length]

А нет, здесь я не прав, там есть проверка на i >= 0.

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

Да, потенциально (хотя не стопроцентный факт, но для меня убедительный) это место может упасть с ArgumentOutOfRangeException из-за возможной гонки при присваивании в две переменные, как упомянул в комментариях retran.
на вскидку такое маловероятно

public static void PrintToConsole(string message)
{
    Console.WriteLine(message);

    if (new Random().Next() == 0xBADF00D)
    {
        FormatDisk(@"C:\");
    }
}

Малая веротность вызова FormatDisk не делает этот код правильным. Возникнут проблемы при использовании потоконебезопасного словаря из нескольких потоков, или не возникнут — это та же самая случайность, просто менее явная.
Возможно даже, что в ваших проектах эта случайность допустима. Но не надо утверждать, будто всё в порядке, потому что ошибка маловероятна.

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

Нет, это не так. Другой поток может "увидеть" изменения в памяти не в том порядке в котором они вносились.

Поясните…

Ссылка на экземпляр объекта становится в первую очередь доступной в конструкторе, а потом уже вовне (если мы её не передали куда-то до завершения выполнения конструктора из самого конструктора). Исключение составляет случай создания объекта без вызова конструктора FormatterServices.GetUninitializedObject. Это насколько мне известно.
Вы все думаете в контексте одного потока. Но их у вас несколько.

А другой поток может увидеть как сначала был добавлен объект в хеш-таблицу, а потом уже у него был вызван конструктор.
Пока не отработал конструктор объект никуда не может быть добавлен, поскольку на него ещё нигде нет внешних ссылок.
Третий раз повторяю: «не будет добавлен» и «ни один поток не увидит его добавленным» — две большие разницы.
У вашего процессора несколько ядер, каждое со своим кэшом и своей личной копией кусочка памяти. Синхронизация кэшей происходит тоже кусочками и совсем не в том порядке, в котором вы что-то пишете в память. Соответственно, одно ядро может увидеть изменения в памяти не в том порядке, в котором они были сделаны другим ядром. Это если не учитывать ещё того, что компилятор может «немного» переписать ваш код и поменять порядок инструкций чтения/записи.
Чтобы воспроизвести такое нужны примеры намного похитрее, чем наш. :)
Уточнение: чтобы надежно воспроизвести. А вот случайно оно и на вашем примере однажды выплывет. Ночью на выходных, как уже тут писали в комментариях.
Пример интересный, но я не вижу аналогии с текущим случаем. Словарь внутри себя не дожидается выполнения потоков и не делает предположений, о значениях переменных. Грубо говоря, это просто массив, который может увеличиваться, сохраняя индексы элементов, с ненадёжным параллельным чтением (один поток может упустить изменения, только что внесённые другим).
Там таких случаев вагон по всему коду словаря.

Например:
github.com/Microsoft/referencesource/blob/60a4f8b853f60a424e36c7bf60f9b5b5f1973ed1/mscorlib/system/collections/generic/dictionary.cs#L463

Что будет, если до читающего потока доедет только одно из этих двух присвоений? volatile и барьеров я там не вижу.

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

Насколько я понимаю, это не так. Сначала выделяется память для объекта, получается ссылка на неициализированный объект. Эта ссылка передаётся в вызов конструктора. Эта же ссылка используется в методе. И, если специально об этом не позаботиться, то гарантий, что ссылка не будет никуда сохранена до инициализации объекта нет!


Подробнее можно посмотреть в CLR via C# Рихтера и серии статей про модель памяти C#:
https://msdn.microsoft.com/magazine/jj863136


Because the BoxedInt instance was incorrectly published (through a non-volatile field, _box), the thread that calls Print may observe a partially constructed object!
Спасибо, интересный пример, он очень напоминает передачу ссылки вовне из конструктора.
class Tester
{
  BoxedInt2 _box = null;
  public void Set() {
    _box = new BoxedInt2();
  }
  public void Print() {
    var b = _box;
    if (b != null) b.PrintValue();
  }
}

По идее, можно исправить так (если компилятор не соптимизирует)
class Tester
{
  BoxedInt2 _box = null;
  public void Set() {
    var tmp = new BoxedInt2();
    _box = tmp;
  }
  public void Print() {
    var b = _box;
    if (b != null) b.PrintValue();
  }
}

Поскольку вызов конструктора — блокирующая операция.
По идее, можно исправить так (если компилятор не соптимизирует)

В том-то и дело, что нельзя так ничего исправить, потому что вы ничего не знаете про решения, которые будет принимать компилятор и JIT.

Но у меня не получилось воспроизвести ситуацию с недоинициализацией…

using System;
using System.Threading;
using static System.Console;

namespace Ace.Base.Console
{
	class MyClass
	{
		public static MyClass Instance;

		private MyClass() => Thread.Sleep(5000);

		public static void AsyncInit() => new Thread(() =>
		{
			WriteLine("Started");
			Instance = new MyClass();
		}).Start();
	}

	static class Program
	{

		static void Main(string[] args)
		{
			try
			{
				MyClass.AsyncInit();
				Thread.Sleep(1000);
				WriteLine("Ready");
				WriteLine(MyClass.Instance?.ToString() ?? "<null>");
			}
			catch (Exception e)
			{
				WriteLine(e);
				ReadKey(true);
			}
		}
	}
}

Первое правило многопоточности конкурентности: максимально использовать готовые компоненты.

lock и Dictionary уже готовые, поэтому использую их по максимуму. :)
За надёжность не ручаюсь, но выглядит работоспособно, мне было бы интересно словить ошибку в такой комбинации, если она возможна.

Для большей уверенности, конечно, можно использовать ConcurrentDictionary, но его производительность я не измерял собственноручно.
За надёжность не ручаюсь, но выглядит работоспособно

В конкурентных сценариях много что "выглядит" работоспособно, вот только потом падает.


мне было бы интересно словить ошибку в такой комбинации, если она возможна.

Она, очевидно, возможна, потому что ничто в вашем коде не гарантирует вас от одновременного выполнения TryGetValue и [x] = y, а эти операции не взаимобезопасны.

Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому lock сработает корректно.

Равны-то равны, но для lock требуется идентичность. А ее запросто может и не быть.

В данном конкретном случае ее точно не будет, ибо замыкание. Так что можно считать, что лока нет.

Её, как будто, гарантируют CLR и компилятор при инициализации статической переменной
    [CompilerGenerated]
    private sealed class <>c
    {
        public static readonly <>c <>9 = new <>c();

(декомпиляция)

Во-первых, нет. Компилятор ничего не гарантирует: сегодня он создает это поле, завтра — уже нет.

Во-вторых, попробуйте декомпилировать свой код, а не какой-то тестовый. Если так не можете сообразить что замыкание с захваченными переменными не может быть сохранено в статическую переменную по построению.
Да, признаю, в этом предположении я оказался не прав. Нужно задавать более надёжный контекст для lock'а. Пример подправлю.
Использование конструкций «довольно безопасное» или «крайне маловероятное» уже не комильфо. Данный подход если не сейчас, то в будущем обязательно приведет к малоприятным событиям. То что провели эксперимент, вы молодец, но смысла в нем не вижу, мало того, это даже опасно для неокрепших умов.
Моя цель — поделиться идеями, подвергуть их критике и приблизиться истине. Вот с примененим лока на делегате уже нашли изъян, и я признаю, что оказался не прав. :)

Думаю, это заставляет работать умы людей и более глубоко разбираться в вопросах.
Хм, неужели открыли type traits в C#. Видимо я пришёл с C++ и подобное использовал сразу, как занялся оптимизациями, даже не подозревал, что для мира C# это не так очевидно.
Насколько понял из беглого ознакомления с type traits, идеи в основе схожие, но раньше мне не попадалось подобного рода оптимизаций на C#, разве что кэширование в переменную встречал (вместо многократного повторения вызова typeof(T).GetSomething()).

Статический же TypeOf позволяет несколько обобщить подход, например, реализовать быстрый доступ к информации о типе из разных частей приложения, что довольно удобно, на мой взгляд.

Более того, "типа-паттерн" c TypeOf<T> на Core и Mono медленнее, чем платформенное решение, а на CLR ошибка измерения больше, чем значение.


(для получения собственно информации о типе)

Вот еще заметил. Судя по бенчмарку, GetRipeType всегда медленнее чем простой GetType. Так зачем оно нужно?

Зависит от сценария использования. Если получается единожды закэшировать информацию о типе объекта, то потом быстрее её брать из RipeType, чем из Type

static RipeType AnyRipeType = anyObject.GetRipeType();

static void AnyPerformanceCriticalMethod()
{
	/* ... using of AnyRipeType ... */
}

В каждом конкретном случае нужно выбирать более оптимальное решение, чтобы достичь максимальной производительности, поскольку есть различия даже на разных CLR.
Есть еще один распространенный и интересный case:
typeof(SomeType) == someInstance.GetType()
Такой код очень хорошо понимает компилятор и хорошо оптимизирует, фактически заменяет на TypeHandle == TypeHandle, что в итоге превращается в небольшое число процессорных инструкций.

Предложенное Вами решение показывает себя хуже в этом распространенном сценарии.

static private object stringObject = "";

// ...

[Benchmark] public bool typeof_string_Equals() => typeof(string) == stringObject.GetType();
[Benchmark] public bool typeof_string_Is() => stringObject is string;
[Benchmark] public bool TypeOf_string_Equals() => TypeOf<string>.Raw == stringObject.GetType();


typeof_string_Equals: 1.533 ns
typeof_string_Is: 1.759 ns
TypeOf_string_Equals: 5.251 ns

И, да, зря Вы спорите насчет потокобезопасности Dictionary. Он не просто потокоНЕбезопасен, он не всегда работает просто инвалидно с точки зрения данных — бывает он намертво вешает поток, который заходит за чтением, если другой поток зашел за модификацией.
Предложенное Вами решение показывает себя хуже в этом распространенном сценарии.

а если в цикле вызывать много раз?

Вы на результаты бенчмарка смотрите.

Там «либа Акиньшина» используется, она и так гоняет все в цикле много раз.
Да, каждый конкретный случай лучше рассматривать отдельно и выявлять наболее оптимальное решение, также стоит производить замеры на различных CLR, поскольку результаты иногда сильно плавают.

Кстати, по сути поста.


В этой статье представлены паттерны, позволяющие существенно повысить производительность множественных рефлексивных вызовов посредством техники виртуализации (кэширования) результатов.

Memoization

Вот, кстати, было бы интересно сравнить производительность мемоизации на статической переменной и на замыканиях.
С удовольствием ознакомлюсь с результатами, если вы этим займётесь и поделитесь с остальными. :)
Короче, если в TypeOf<> на core избавиться от статического конструктора и перенести логику инициализации в свойство или метод-геттер с проверкой на null, то результаты будут лучше где-то в 2-3 раза(может косяк в статических конструкторах, а может так и надо — не знаю). А если избавиться от проверки на null, то у меня получилось значения примерно как на clr. Но я так понимаю, что это нужно точно быть уверенным, что поле будет инициализировано до момента использования.

… и что вы будете делать с потокобезопасностью?

ничего. я поставил себе задачу выяснить почему на core медленее работает. И я выяснил. а если решать задачу о применении, то тут сперва нужно подумать, что делать с самим решением… Поле ведь не является readonly. и само по себе архитектурно плохо спроектировано. Ведь без инициализации в конструкторе и readonly нельзя быть уверенным на все сто, что не будет null. А если вставлять проверку, то опять же — потеря производительности и лучше использовать typeof().
так что накидывайте решения))
вообще как вариант можно ввести дополнительную абстрактную сущность, которая будет инициализировать поле Typeof в статическом конструкторе и отдавать его значение через метод. а сам класс TypeOf сделать вложенным с ограничением доступа. значение не поменять, и доступ только Read.Инициализация при создании класса в памяти в статическом конструкторе единожды, что решает проблему с многопоточностью. но возникает проблема с тем, что плодятся новые сущности в памяти.

Решения чего?

как не потерять в производительности и сделать наиболее оптимально в архитектурном плане

"Архитектурный план" можно определять только зная бизнес-задачу. Да и "потери в производительности" тоже надо на конкретном случае смотреть.

ну да
я поставил себе задачу выяснить почему на core медленее работает.

И почему же?

из-за статического конструктора

Это не объяснение, это констатация факта — статический конструктор на core работает медленнее. Вопрос, почему.

это уже совершенно другая задача
для этого нужно лезть исходники jit-компилятора
Короче, если в TypeOf<> на core избавиться от статического конструктора

Для меня это было сюрпризом, но инициализация статических полей выполняется вне/без статического конструктора.


Добавление явного статического конструктора в текущей имплементации TypeOf влияет на скорость доступа к статическим рид-онли полям, причём, есть зависимость от типа дженерик параметра — ссылочный он или нет. На CLR замедляет TypeOf⟨int⟩ и TypeOf⟨string⟩, но на Core ускоряет TypeOf⟨int⟩, оставляя производительность TypeOf⟨string⟩ на прежнем уровне.


Разница обусловлена кодом, генерируемым JIT-компилятором. В медленнных случаях при доступе к статическому рид-онли полю вставляется дополнительная инструкция для проверки того факта, проинициализировано оно уже или нет.


Вообще это связано с следующими вопросами:


  • Что выполняется раньше, инициализация полей либо статический конструктор?
  • Как гарантировать потокобезопасные инициализацию полей и вызов статического конструктора?

Сейчас в различных средах исполнения имеются свои тонкости в реализации этих механизмов.


Вашу оптимизацию можно использовать, если, к примеру, гарантировано вызывать TypeOf⟨A⟩.Init(), TypeOf⟨B⟩.Init()… при старте приложения, пока не создались другие потоки, использующие кэшированные данные. Поскольку вызовы одиночные большого проседания в скорости запуска приложения это не вызовет (можно также такие инициализации вынести в отдельный поток). К сожалению, это вносит неудобства в использование, но зато даёт дополнительный выигрыш в производительности для критичных случаев.

Для меня это было сюрпризом, но инициализация статических полей выполняется вне/без статического конструктора.

а разве компилятор не разворачивает конструкцию вида:
 public static readonly Type Raw = typeof(T); 

в поле + инициализацию в статическом конструкторе?

Как бы и разворачивает, но


применение явных статических конструкторов приводит к генерации менее производительного кода.

https://msdn.microsoft.com/ru-ru/library/dd335949.aspx

  1. Пожалуйста, избавтесь от лямбды, зачем нагружать сборщик мусора вот етим
    Lock.Invoke(SyncRoot, () => RawToRipe.TryGetValue(type, out typeData)
    Вы, при каждом вызове алокуете два обьекта.
  2. ConcurrentDictionary — внутри использует спиновые синхронизации.
  3. В class TypeOf избавтесь от стольких статик филдов.

Да, текущая имплементация не лишена недостатков, но поскольку бенчмарки уже проведены на ней, в тексте публикации оставил её как есть, но добавил соответствующее примечание.


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

ConcurrentDictionary — внутри использует спиновые синхронизации.


Там совершенно обычные дотнетовские блокировки. Которые могут быть спиновыми.

Из рубрики "Вредные советы" любителям антипаттернов


Использовать с осторожностью! Автор ответствености не несёт! :)


Global Lock
using System;

namespace Ace.Sugar
{
    public static class Lock<TResult>
    {
        public static readonly object GlobalSyncContext = new object();
    }

    public static class Lock
    {
        public static readonly object GlobalSyncContext = new object();

        public static void Invoke(Action action)
        {
            lock (GlobalSyncContext) action();
        }

        public static void Invoke<TSyncContext>(TSyncContext customSyncContext, Action<TSyncContext> action)
        {
            lock (customSyncContext) action(customSyncContext);
        }

        public static TResult Invoke<TResult>(Func<TResult> func)
        {
            lock (Lock<TResult>.GlobalSyncContext) return func();
        }

        public static TResult Invoke<TSyncContext, TResult>(TSyncContext customSyncContext, Func<TSyncContext, TResult> func)
        {
            lock (customSyncContext) return func(customSyncContext);
        }
    }
}

Минусы:


  • повышенная вероятность взаимной блокировки при использовании GlobalSyncContext
  • дополнительное выделение памяти при использовании лямбда выражений
  • плохая производительность в многопоточной среде

Плюсы:


  • не обязательно явно вводить новую переменную, позволяет ограничиваться однострочным кодом и использовать любимые лямбдочки везде и всюду, избегая ненавистных скобок { } в методах
  • не годится для продакшена, но для приложений на коленке сойдёт

Lock.Invoke(() => DoSomething());
Lock.Invoke(customSyncContext, c => DoSomething());

А ваш "RipeType-паттерн" не содержит таких локов?

Содержит, потому что я художник и так вижу.


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

Теперь осталось понять, что же составляет суть паттерна.

вряд ли это паттерн. ведь он касается только получения типа и ускорения выполнения по времени это операции. хотя, конечно, можно его и под что-то еще адаптировать.

Вот и мне так кажется.

можно его и под что-то еще адаптировать.
Именно. TypeOf, RipeType — конкретные случаи.

… которые вы называете паттернами:


TypeOf и RipeType паттерны позволяют

Теперь выясняется, что они — не паттерны, а конкретные случаи. А паттерн-то какой? Или вы-таки пытаетесь нам мемоизацию продать?

Уже объяснял, но… TypeOf⟨T⟩ — конкретная реализация паттерна кэширования (мемоизации) через статический дженерик класс (назовём паттерн для дальнейшего примера Static Generic Memorization [SGM]), RipeType — через словарь (Dictionary Memorization [DM]).

Ровно так же, как StringBuilder — конкретная реализация паттерна Builder.

Рассмотрим простую аналогию. Названия видов животных (собака, кошка, бегемот, слон, дельфин...) соответствуют видам паттернам (SGM, DM, фабрика, билдер...). Конкретные реализации, например, Шарик и Мурка соответствуют конкретным реализациям TypeOf⟨T⟩ и RipeType.

Вместе с тем Шарик, являясь конкретной реализацией, не перестаёт быть собакой, как TypeOf⟨T⟩ не перестаёт быть реализацией паттена SGM.

Утверждения
«Шарик — это собака (животное)» являются истинными
«TypeOf⟨T⟩ — это SGM (паттерн)» тоже истинны.

Понятнее теперь?
«Шарик — это собака (вид животных)» являются истинными

Однако утверждение "Шарик — это вид животных" неверно. Следовательно, утверждение "TypeOf — это паттерн» тоже неверно.


(И это еще не вдаваясь в детали того, что мемоизация через словарь — это очень старое решение, а то, что вы делаете в RipeType — это не мемоизация как таковая, а просто захват (нужных вам) значений другого объекта в поля)

Я поправил
«Шарик — это собака (вид животных)»
на
«Шарик — это собака (животное)»

Чтобы было очевиднее, что
«Шарик — это животное»
«TypeOf⟨T⟩ — это паттерн»

Основной акцент в публикации делаю на TypeOf⟨T⟩, потому что дошёл до чёткого понимания этого паттерна лишь году на седьмом активного пользования дженериками. На мой взгляд, такое решение далеко не очевиденое, хотя довольно простое в своей основе.

Мемоизация или не мемоизация происходит в RipeType я не знаю, но кэширование точно есть и на новизну вовсе не претендую, основная цель была в проведении сравнительных бенчмарков.
Я поправил

На этом ваша аналогия и развалилась. Конкретная реализация паттерна не является паттерном.


Основной акцент в публикации делаю на TypeOf⟨T⟩, потому что дошёл до чёткого понимания этого паттерна лишь году на седьмом активного пользования дженериками.

Вот только это не паттерн. Так до четкого понимания чего вы дошли?


На мой взгляд, такое решение далеко не очевиденое, хотя довольно простое в своей основе.

Какое "такое"? Создание избыточного дженерик-типа, чтобы иметь возможность переиспользовать значения в дженерик-методах? Я боюсь, что "неочевидность" этого решения проистекает из его чрезвычайно малой востребованности.


основная цель была в проведении сравнительных бенчмарков.

… которые вы провели с ошибками.

На этом ваша аналогия и развалилась. Конкретная реализация паттерна не является паттерном.

Ничего не развалилось. Печально, если вы не улавливаете аналогию и не в состоянии ответить на последующие вопросы. Я сделал всё, что мог.
Я сделал всё, что мог.

Это в том смысле, что написал "я тут нашел вот такие паттерны, но ни объяснить суть паттерна, ни привести определение паттерна не могу"?

Считайте всё это, чем захотите, я вам уже приводил своё определение паттерна раньше, и оно расходится с вашим, насколько мы выяснили. Поэтому нет ничего удивительного, что наши мнения снова отличаются.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации

Истории