Комментарии 229
Dictionary<Type, RipeType>
— непотокобезопасный контейнер. Нельзя его в одном потоке читать, а в другом в то же самое время писать! Да и завязываться на то что компилятор закеширует передаваемый в Lock.Invoke
делегат тоже не стоит...
Почему бы не использовать ConcurrentDictionary вместо велосипеда?
ConcurrentDictionary
использовать, если нужно.По началу у меня самого были подозрения насчёт такого решения, но при более детальном анализе я пришёл к выводу, что оно довольно безопасное. Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке… Мне самому интересно о нём узнать, если он существует.
Да и завязываться на то что компилятор закеширует передаваемый в Lock.Invoke делегат тоже не стоит...
Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому
lock
сработает корректно.Буду признателен, если вы всё же укажете на возможный сценарий, приводящий к ошибке…
Пока один поток меняет словарь, второй из него читает и получает мусорные данные. Например, не до конца заполненный RipeType. Или падает с NPE из-за видимого нарушения внутренней структуры словаря.
class AnyClass
{
public static AnyClass Instance;
public AnyClass()
{
Instance = this;
/* ... */
}
}
Но это не наша ситуация, поэтому вариант с недоинициализированным RipeType отпадает.
По логике вещей, при чтении структура словаря не может быть нарушена, даже если оно идёт из разных потоков. При параллельной записи тоже, поскольку есть lock. Остаётся лишь случай чтения в момент записи… Мне думается, что словарь не бросит исключение от такого, а если вдруг чтение произошло до момента вставки только что созданного экземпляра и вернулся null, то мы направляемся в lock и дожидаемся завершения вставки, после чего повторяем чтение и получаем уже созданный экземпляр.
З. Ы. Блокировка на делегате — это совсем жесть, конечно.
С непривычки да, но как бы должно работать, поскольку компилятор обеспечивает однозначный и потокобезопасный контекст блокировки.
Но в данном решении атомарность и не требуется за счёт повторного чтения под локом.
То есть то, что у вас чтение бывает для разных ключей, вы и забыли, да?
Боттлнек на пустом месте, да.
… вот именно поэтому не надо придумывать свою реализацию, не разобравшись в проблеме.
Вот у вас есть словарь, в нем есть значение для ключа A. Теперь к вам одновременно пришли запросы для ключей A и B. Первый попадет в чтение, второй — в запись, и они могут идти строго одновременно, потому что на первый не распространяется лок.
Не, не должно. У вас каждый раз при входе в метод будет создаваться новый экземпляр скрытого типа, и поэтому lock
всегда будет получать новый объект.
Мне думается, что словарь не бросит исключение от такого
Не, не бросит. Просто тихо вернет не то значение. Вы внутрь TryGetValue
никогда не заглядывали?
Я прямо говорю, что мной выбрано такое решение в целях эксперимента и обсуждения, поскольку есть подозрение, что оно рабочее.
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
точно перемешивается.
Выглядит так, что в худшем случае мы можем лишь потерять элемент, уже находящийся в словаре, что приведёт к его пересозданию извне
… а поскольку пересоздание внутри себя тоже не потокобезопасно, там можно получить исключение. И это не единственный возможный сценарий.
исключения точно не будет
Вы проверил все возможные сценарии, со всеми расположениями переменных? Например, что случится, если у вас происходит два одновременных добавления, и два потока одновременно заберут ссылку на следующий свободный элемент, и там окажется хэш-код и ключ от одного элемента, а значение — от другого?
Вы, повторюсь, забыли, что у вас параллельно еще присвоения идут?
Запись элементов идёт только под 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 создается свой тип.
Оставляю на ваш суд.
Смешно, да. Когда я вам говорю, что замеры бессмысленны, вы просите это доказать фактами. А когда я спрашиваю, что они значат, вы оставляете это на мой суд.
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);
}
А смысл что-то мерять? Это код, который выполняет поставленную задачу оптимальным с точки зрения написания кода образом. Я, напомню, говорил о качестве, а не о производительности вашего кода.
Если у написанного мной кода обнаружатся проблемы производительности, я буду их разбирать с профайлером и смотреть на профайлер. Абстрактные задачи "сделайте так, чтобы этот метод выполнялся в два раза быстрее" решать очень вредно, потому что внезапно может выясниться, что самое дешевое решение — закэшировать нужное значение снаружи метода.
(Все это, кстати, еще и потому, что написать хороший тест на производительно в условиях многопоточности — то еще искусство)
И насчёт «ужасного кода» вот яркий пример
EqualityComparer<T>.Default.Equals(a, b);
Самое дешёвое и универсальное решение [...] это замена этой конструкции на статическую версию TypeOf
Не доказано. Хотя бы потому, что не определена метрика "дешевизны".
Я вам больше того скажу, "решение" — оно всегда для проблемы, никогда не само по себе. А проблема пока не найдена.
Кстати, в обсуждаемом выше примере (AnyMethod<T>
) и рефлексии-то, по сути, нет.
А, если я не ошибаюсь, смысл в том, чтобы он вызывался один раз.
Это неправильная постановка задачи. На двух платформах из трех вызов typeof(T)
быстрее предлагаемого решения со статическим классом, а на третьей слишком большая ошибка измерения, чтобы был смысл об этом говорить.
Под «достаточно одного T» имелось в виду, что у метода один дженерик параметр, а различных значений T пусть будет от 10 до 100.
В этот момент я заявляю, что вы страдаете херней и если у вас реально боттлнек в этом месте, то любой «паттерн» проиграет предзаполненному lookup table на Dictionary без блокировок вообще.
И заодно подумайте, почему
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⟩.
Вот только EqualityComparer<T>
— не статический класс, и его (основной) задачей не является кэширование. Поэтому он не может быть частным случаем вашего паттерна.
Насколько понимаю, вы имели ввиду под «ужасным» кодом именно этот общий случай
Нет, в первую очередь под ужасным кодом я имел в виду тот код, который я комментировал.
Нет, не получится. Задача EqualityComparer<T>
— совсем не в кэшировании.
EqualityComparer.GetDefault<T>()
.Нельзя: класса EqualityComparer
не существует.
EqualityComparerProvider.GetDefault<T>()
со словарём внутри, который предоставляет инстенсы EqualityComparer⟨T⟩? Почему выбрано иное решение?
Я вот вообще-то думал, что мы задачу про мемоизацию функции решаем.
UPD Я вот считаю, что кэш без вытеснения — и не кэш вовсе. Поэтому о задаче кэширования в вашем случае вообще можно не говорить.
Я вот вообще-то думал, что мы задачу про мемоизацию функции решаем.
Так и я же говорю, что в определённых дженерик случаях она прекрасно решается с высокой производительностью стататическим дженерик классом с рид-онли полем.
… но нет никакого повода считать это решение "хорошим". Хотя бы потому, что вы немножко замучаетесь генерить по статическому классу на каждую функцию.
"определенные дженерик случаи" — это очень смешно. Так определенные или дженерик? И если "там" требуется "высокая производительность", то лучший ли это способ ее достигнуть?
Повторюсь, оптимизационная задача "сделайте метод быстрее" неверна в корне. Смотреть надо на операцию (в прикладных терминах), которая происходит медленно, и разбираться, как ее оптимизировать. И ценой чего ее оптимизировать.
В своей практике я дошёл до того момента, когда мне потребоволась кэшировать информацию о типах, чтобы достичь лушей производительности, и TypeOf с RipeType мне в этом помогли.
Дальше я просто поделился опытом с другими людьми, потому что до этих решений дошёл далеко не сразу, пришлось проводить различные замеры, эксперементировать с реализациями, разбираться с деталями. И своей статьёй, возможно, кому-то сохраню немало времени в дальнейшем.
У меня нет цели доказывать кому-то что-то или навязывать. Нравится решение — бери и используй, не нравится — пробуй другое.
Где я написал, что так нужно делать везде и всегда?
Вот здесь:
Самое дешёвое и универсальное решение [...] в случае использования множественных рефлексивных вызовов на основе typeof в различных частях приложения — это замена этой конструкции на статическую версию TypeOf
[...]
В своей практике я дошёл до того момента,
Давайте определимся: в рабочей практике или в каких-то вольных исследовательских проектах? Иными словами, код, который вы нам показываете — он production ready или нет?
мне потребоволась кэшировать информацию о типах, чтобы достичь лушей производительности, и TypeOf с RipeType мне в этом помогли.
Теперь осталось выяснить, не было ли лучшего способа решить вашу проблему.
разбираться с деталями.
Мы же уже выяснили, что вы не делали анализа своих находок?
Однозначно, при решении более общей задачи кэширования на основе дженерик ключа я буду рассматривать подобный подход в качестве одного из потенциальных решений.
Теперь осталось выяснить, не было ли лучшего способа решить вашу проблему.К сожалению, способа получения информацию о типе минуя typeof или GetType я не знаю, но знаю, как её закэшировать для наиболее быстрого доступа в дальнейшем.
Мы же уже выяснили, что вы не делали анализа своих находок?По моим личным критерия анализа, решения мне подходят. Вы свои критерии знаете куда лучше, поэтому применимость находок для себя сможете определить сами.
Что касается typeof, то в своих вольных проектах я везде заменил его на TypeOf
Вот именно слово "везде" и говорит о том, что вы не делали анализа, а просто что-то себе придумали и используете. Хотя нет (ну или я не знаю) ни одной оптимизации, которая была бы универсальна.
(в частности, в дженерик-типах ваше решение избыточно, но вы об этом не задумались)
что для меня это самое оптимальное и универсальное решение
… по удобным вам критериям, или, проще говоря, "я так решил".
К сожалению, способа получения информацию о типе минуя typeof или GetType я не знаю, но знаю, как её закэшировать для наиболее быстрого доступа в дальнейшем.
Вот только способов "кэширования" намного больше, чем вы тут рассмотрели.
По моим личным критерия анализа, решения мне подходят.
Ваши личные критерии мы уже как-то обсуждали.
Иногда по определённым вопросам спор с вами напоминает мне парадокс Кэррола.
Перед вами очевидные суждения:
А. чем меньше времени занимает событие, тем оно быстрее происходит
Б. определённые вызовы сравниваемых методов занимают меньше времени, чем другие
В. значит, эти вызовы быстрее
Вы же мне предлагаете доказывать справедливость В через кучу других суждений, уводящих в сторону.
Да нет, справедливость В (в его точной формулировки: "конкретные вызовы быстрее в конкретной ситуации") доказывает бенчмарк. Проблема не в этом утверждении, проблема в том, что вы заменили все вызовы typeof
на ваш TypeOf
, не рассмотрев другие варианты локальных оптимизаций, которые могли бы дать еще больший выигрыш (или быть оптимальным по другим критериям). А есть еще не-локальные оптимизации.
И, наконец, я вам предлагаю рассмотреть задачу верхнего уровня, а не заниматься микрооптимизациями без разбора.
Варианты оптимизаций я рассматриваю для каждого случая индивидуально. Касательно typeof у меня был ряд сценариев, где его избежать нельзя. Судя по результатам бенчмарков в терминах скорости выполнения, использование TypeOf в большинстве случаев даёт ощутимый выигрыш.
Есть потенциальный минус в виде статических объектов, постоянно находящихся в памяти, однако для моих приложений это допустимо.
Варианты оптимизаций я рассматриваю для каждого случая индивидуально. Касательно typeof у меня был ряд сценариев, где его избежать нельзя.
И все равно "везде заменил"?
Судя по результатам бенчмарков в терминах скорости выполнения, использование TypeOf в большинстве случаев даёт ощутимый выигрыш.
Подозреваю, что это зависит от сценария использования.
Да, везде заменил, потому что медленнее работать не будет в моих случаях, плюс общность появляется, а не где-то typeof, а где-то TypeOf.
А плата в виде слегка повышенное потребления памяти вполне для меня допустима.
Да, везде заменил, потому что медленнее работать не будет в моих случаях, плюс общность появляется, а не где-то typeof, а где-то TypeOf.
Круто получается. "Был ряд сценариев, где избежать нельзя", но "везде заменил".
А уж про "медленеее работать не будет в моих случаях" мне и вовсе сказать нечего — мы про ваши случаи ничего не знаем, но судя по вашим примерам кода, от моих случаев они очень далеки.
Все мои случаи доступны в открытых репозиториях с кодом — изучайте при желании.
Нужно сделать быстрее в два раза при множественных вызовах.
На первом году пользования дженериками, ровно в тот момент, когда понял, что для каждого варианта дженерика в .net создается свой тип.
вроде же не всегда. там разве не используются различные техники оптимизации?
С практической точки зрения — всегда. Когда у вас есть
class Some<T>
{
static Blah _foo;
}
вы можете быть уверенными, что значение _foo
будет "разделено" между всеми экземплярами с одним и тем же T
, и изолировано для разных T
.
Вот здесь мне сложно говорить, я не вдавался в эти детали (еще и потому, что для моих целей они не критичны). Описанное выше поведение — оно изначально не очень интуитивно, но к нему привыкаешь, и есть много мест, где им пользуются.
Может быть, и могу. Но дело в том, что у меня такая позиция в программировании — испытывать на прочность самые неожиданные сценарии и варианты, а не ходить по проторенным и безопасным тропинкам. :)
Меня в институте учили, что испытание/эксперимент, это:
1. Подробное изучение всей доступной информации об объекте эксперимента.
2. Выдвижение четкой гипотезы, базирующейся на известной информации, а не на предположениях.
3. Разработка и проведение повторяемых экспериментов, в том числе опровергающих гипотезу.
1. изучена работа typeof и Type
2. выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях
3. разработан и проведён ряд повторяемых экспериментов, в том числе опровергающих гипотезу
Получены результаты и предоставлены на рассмотрение широкому сообществу. :)
выдвинута чёткая гипотеза, что TypeOf и RipeType могут работать быстрее в некоторых сценариях
"… некоторых сценариях". Очень "четкая" гипотеза.
Но вы так и не определили, почему оно имеет разную производительность на разных фреймворках.
(Это я даже не начал вдаваться в статистический инструментарий под формулировкой гипотезы и ее проверкой)
В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов (некоторые добавляют дополнительную проверку на то, проинициализировано ли поле, что сказывается на производительности). Тема также тесно связана с добавлением статических конструкторов, у которых, как оказывается, есть ряд подводных камней…
Здесь вообще разговор для отельной статьи, так что при желании можете сами углубиться в этот вопрос, а потом поделиться результатами с остальными.
Вообще-то определил, но, к сожалению, теперь вы не сможете просмотреть детали в открытом доступе, которые раньше находились тут.
Ну да, а публикации на хабре они недостойны, дадада.
В общих словах, различные CLR генерируют неодинаковый код во время JIT-компиляции при доступе к статическим рид-онли полям классов
И снова вопрос: почему?
Здесь вообще разговор для отельной статьи, так что при желании можете сами углубиться в этот вопрос
Мне-то это зачем? Это вы выдвигаете какие-то гипотезы, а у меня проблемы с производительностью совсем в других местах.
Ну да, а публикации на хабре они недостойны, дадада.Достойны, но вы ж должны понимать, что статьи я пишу лишь в своё свободное время for free. Подготовка материалов даже к этой публикации потребовала уйму времени и сил. Да и пишу в основном о том, с чем сам сталкиваюсь на практике. Нужно или не нужно, люди сами уже для себя решат.
На вопрос почему разные CLR генерируют отличающийся код, исчерпывающего ответа не нашёл и в оригинальной дискуссии его тоже не оставили.
Подготовка материалов даже к этой публикации потребовала уйму времени и сил.
Печально.
И мы ещё даже не начали за когерентность кэшей разговаривать.
Там есть дофига мест, где используется модуль от текущего (меняющегося) размера, так что можно получить много боли. Получить не тот айтем возвращенным так просто не выйдет, но вот нарваться на IndexOutOfRange или Duplicate Key — да.
Мне так видится реализация.
По сути TryGetValue гарантирует, что не будет исключений из какого бы мы потока не работали со словарём.
Не гарантирует. В текущей реализации мы не нашли места, где может быть исключение — это да. Но никаких гарантий нет.
Но у вас же и от одновременных присвоений нет никакой защиты.
1. Гаранития того, что коллекция вообще будет работать в условиях нескольких потоков
2. Гарантия того, что при записи/удалении/замене элемента одним потоком, второй изменения сразу же увидит
Сейчас я придерживаюсь второго, более сильного. Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.
Словарь, не являясь потокобезопасным классом, способен работать в условиях нескольких потоков, но может давать ненадёжные результаты.
Зачисляете Вы деньги на свой счет периодически, а может иногда зачислять Ваши деньги на чужой счет. Это хорошо?
Исключений нет, как выяснили, другой элемент тоже не придёт в результате.
Это только в текущей реализации. Завтра ее поменяют — и у вас все упадет (это, если что, говорит человек, у которого именно такое случилось при апгрейде с 4.7.1 на 4.7.2).
Вы забыли одно важное дополнение: при выполнении предусловий.
Теперь открываем документацию:
A Dictionary<TKey, TValue> can support multiple readers concurrently, as long as the collection is not modified.
Выделенное мное условие у вас не выполняется. Я вам больше того скажу, в общем случае TryGetValue
может упасть и сейчас — если параллельно Clear
вызвать.
А нет, здесь я не прав, там есть проверка на i >= 0
.
… впрочем, это все ровно до тех пор, пока кто-нибудь не прикрутит к словарю тримминг.
на вскидку такое маловероятно
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 и барьеров я там не вижу.
Ссылка на экземпляр объекта становится в первую очередь доступной в конструкторе, а потом уже вовне (если мы её не передали куда-то до завершения выполнения конструктора из самого конструктора).
Насколько я понимаю, это не так. Сначала выделяется память для объекта, получается ссылка на неициализированный объект. Эта ссылка передаётся в вызов конструктора. Эта же ссылка используется в методе. И, если специально об этом не позаботиться, то гарантий, что ссылка не будет никуда сохранена до инициализации объекта нет!
Подробнее можно посмотреть в 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();
}
}
Поскольку вызов конструктора — блокирующая операция.
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);
}
}
}
}
Первое правило многопоточности конкурентности: максимально использовать готовые компоненты.
За надёжность не ручаюсь, но выглядит работоспособно, мне было бы интересно словить ошибку в такой комбинации, если она возможна.
Для большей уверенности, конечно, можно использовать ConcurrentDictionary, но его производительность я не измерял собственноручно.
За надёжность не ручаюсь, но выглядит работоспособно
В конкурентных сценариях много что "выглядит" работоспособно, вот только потом падает.
мне было бы интересно словить ошибку в такой комбинации, если она возможна.
Она, очевидно, возможна, потому что ничто в вашем коде не гарантирует вас от одновременного выполнения TryGetValue
и [x] = y
, а эти операции не взаимобезопасны.
Если в разных потоках создать два делегата от одного метода, то они будут равны, поэтому lock сработает корректно.
Равны-то равны, но для lock требуется идентичность. А ее запросто может и не быть.
В данном конкретном случае ее точно не будет, ибо замыкание. Так что можно считать, что лока нет.
[CompilerGenerated]
private sealed class <>c
{
public static readonly <>c <>9 = new <>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.
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. Он не просто потокоНЕбезопасен, он не всегда работает просто инвалидно с точки зрения данных — бывает он намертво вешает поток, который заходит за чтением, если другой поток зашел за модификацией.
Предложенное Вами решение показывает себя хуже в этом распространенном сценарии.
а если в цикле вызывать много раз?
Кстати, по сути поста.
В этой статье представлены паттерны, позволяющие существенно повысить производительность множественных рефлексивных вызовов посредством техники виртуализации (кэширования) результатов.
… и что вы будете делать с потокобезопасностью?
Решения чего?
я поставил себе задачу выяснить почему на core медленее работает.
И почему же?
Короче, если в 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);
в поле + инициализацию в статическом конструкторе?
- Пожалуйста, избавтесь от лямбды, зачем нагружать сборщик мусора вот етим
Вы, при каждом вызове алокуете два обьекта.Lock.Invoke(SyncRoot, () => RawToRipe.TryGetValue(type, out typeData)
- ConcurrentDictionary — внутри использует спиновые синхронизации.
- В class TypeOf избавтесь от стольких статик филдов.
Да, текущая имплементация не лишена недостатков, но поскольку бенчмарки уже проведены на ней, в тексте публикации оставил её как есть, но добавил соответствующее примечание.
Думаю, каждый сможет адаптировать её под свои конкретные требования, чтобы достичь оптимальной производительности.
ConcurrentDictionary — внутри использует спиновые синхронизации.
Там совершенно обычные дотнетовские блокировки. Которые могут быть спиновыми.
Из рубрики "Вредные советы" любителям антипаттернов
Использовать с осторожностью! Автор ответствености не несёт! :)
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 паттерны позволяют
Теперь выясняется, что они — не паттерны, а конкретные случаи. А паттерн-то какой? Или вы-таки пытаетесь нам мемоизацию продать?
Ровно так же, как StringBuilder — конкретная реализация паттерна Builder.
Рассмотрим простую аналогию. Названия видов животных (собака, кошка, бегемот, слон, дельфин...) соответствуют видам паттернам (SGM, DM, фабрика, билдер...). Конкретные реализации, например, Шарик и Мурка соответствуют конкретным реализациям TypeOf⟨T⟩ и RipeType.
Вместе с тем Шарик, являясь конкретной реализацией, не перестаёт быть собакой, как TypeOf⟨T⟩ не перестаёт быть реализацией паттена SGM.
Утверждения
«Шарик — это собака (животное)» являются истинными
«TypeOf⟨T⟩ — это SGM (паттерн)» тоже истинны.
Понятнее теперь?
«Шарик — это собака (вид животных)» являются истинными
Однако утверждение "Шарик — это вид животных" неверно. Следовательно, утверждение "TypeOf — это паттерн» тоже неверно.
(И это еще не вдаваясь в детали того, что мемоизация через словарь — это очень старое решение, а то, что вы делаете в RipeType — это не мемоизация как таковая, а просто захват (нужных вам) значений другого объекта в поля)
«Шарик — это собака (вид животных)»
на
«Шарик — это собака (животное)»
Чтобы было очевиднее, что
«Шарик — это животное»
«TypeOf⟨T⟩ — это паттерн»
Основной акцент в публикации делаю на TypeOf⟨T⟩, потому что дошёл до чёткого понимания этого паттерна лишь году на седьмом активного пользования дженериками. На мой взгляд, такое решение далеко не очевиденое, хотя довольно простое в своей основе.
Мемоизация или не мемоизация происходит в RipeType я не знаю, но кэширование точно есть и на новизну вовсе не претендую, основная цель была в проведении сравнительных бенчмарков.
Я поправил
На этом ваша аналогия и развалилась. Конкретная реализация паттерна не является паттерном.
Основной акцент в публикации делаю на TypeOf⟨T⟩, потому что дошёл до чёткого понимания этого паттерна лишь году на седьмом активного пользования дженериками.
Вот только это не паттерн. Так до четкого понимания чего вы дошли?
На мой взгляд, такое решение далеко не очевиденое, хотя довольно простое в своей основе.
Какое "такое"? Создание избыточного дженерик-типа, чтобы иметь возможность переиспользовать значения в дженерик-методах? Я боюсь, что "неочевидность" этого решения проистекает из его чрезвычайно малой востребованности.
основная цель была в проведении сравнительных бенчмарков.
… которые вы провели с ошибками.
На этом ваша аналогия и развалилась. Конкретная реализация паттерна не является паттерном.
Ничего не развалилось. Печально, если вы не улавливаете аналогию и не в состоянии ответить на последующие вопросы. Я сделал всё, что мог.
Я сделал всё, что мог.
Это в том смысле, что написал "я тут нашел вот такие паттерны, но ни объяснить суть паттерна, ни привести определение паттерна не могу"?
typeof(T) vs. TypeOf⟨T⟩