каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.
Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?
Нужна ли в данном случае синхронизация?
ET>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car. ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ? ET>Нужна ли в данном случае синхронизация?
Классика.
Первый метод прочитал car и начал работать.
Второй метод прочитал car и начал работать.
Первый метод записал измененный car.
Второй метод записал свой car — и стер все изменения, записанные первым методом.
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Здравствуйте, LaptevVV, Вы писали:
ET>>Нужна ли в данном случае синхронизация? LVV>Классика. LVV>Первый метод прочитал car и начал работать. LVV>Второй метод прочитал car и начал работать. LVV>Первый метод записал измененный car. LVV>Второй метод записал свой car — и стер все изменения, записанные первым методом.
Что я делаю не так, почему не могу воспроизвести "классику"?
class Program
{
static void Main(string[] args)
{
const int iterationCount = 10000000;
var car = new Car();
var priceTask = Task.Run(
() =>
{
for (int i = 0; i < iterationCount; i++)
CalculatePrice(car);
});
var useTimeTask = Task.Run(
() =>
{
for (int i = 0; i < iterationCount; i++)
CalculateUseTime(car);
});
Task.WaitAll(priceTask, useTimeTask);
Console.WriteLine(car.Price);
Console.WriteLine(car.UseTime);
}
static void CalculatePrice(Car car)
{
car.Price += 1;
}
static void CalculateUseTime(Car car)
{
car.UseTime += 1;
}
}
class Car
{
public int Price;
public int UseTime;
}
Здравствуйте, e.thrash, Вы писали: ET>Нужна ли в данном случае синхронизация?
Нет (при условии, что оба потока не разделяют друг с другом какие-либо изменяемые данные).
Но есть нюансЪ: активная запись-чтение по соседним адресам приводит к накладным расходам на синхронизацию линеек кэша.
Воспроизвести можно, но нужно постараться.
Во-первых, на современных ноутбуках надо включать режим "максимальная производительность" и запитывать от сети. Иначе один и тот же код при последовательных запусках выдаёт 5-15% разницы, если утыкается в CPU и до 40% — если в память. Разумеется, это для коротких бенчмарков, для длинных разница сглаживается.
Во-вторых, надо угадать с аллокациями. Чтобы показать что проблема именно в линейке кэша, надо уложить два инстанса в емнип соседние 64 байт (для интелов). Не всегда удаётся, от запуска к запуску вариант TwoInstancesNear может скакать туда-сюда.
ET>>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car. ET>>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ? ET>>Нужна ли в данном случае синхронизация? LVV>Классика.
Лебединое? Принц Датский? LVV>Первый метод прочитал car и начал работать.
Отлично. LVV>Второй метод прочитал car и начал работать.
Так-так. LVV>Первый метод записал измененный car.
Какой изменённый? LVV>Второй метод записал свой car — и стер все изменения, записанные первым методом.
Какой свой?
Профессор не слышал про указатели?
Или если нет звёздочки — то всё, копия?
Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю...
Здравствуйте, Sharov, Вы писали:
S>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю...
JIT довольно предсказуем. В смысле, оптимизации примерно одни и те же, что в 4.0 x86, что на свежем ryuJit. На старом x64 местами была беда, но сейчас про это можно забыть с чистой совестью.
Ну и знать — не значит "нужно обязательно использовать". Скорее наоборот, знать — это видеть косяки _до_ того, как они выстрелят. Как в этом топике или в примерах с интерполяцией строк
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю... S>JIT довольно предсказуем. В смысле, оптимизации примерно одни и те же, что в 4.0 x86, что на свежем ryuJit. На старом x64 местами была беда, но сейчас про это можно забыть с чистой совестью.
Слишком гранулярная оптимизация для managed языков. Скорее всего я не прав, но моя точка зрения такая: вот мы оптимизируем на уровне линий кэша, вот у нас имеется сторонняя инфраструктура (jit+gc), нами не контролируемая. Каждая из этих структур также работает с кэшом и проч. периферией. Jit более менее понятен, ибо один раз компилирует "раз и навсегда", т.е. temporal locality у нас не пострадает. А вот допустим мы закладываемся на spatial locality в нашем коде, не учитывая инфраструктуру. А тут бац, и гц начинает свою работу ломая все наши предположения об выполнении нашего кода и всяческих spatial locality. Такое возможно или я написал бред?
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, Sinix, Вы писали:
S>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю...
jit может пустить расчет каждого свойства в отдельном потоке или наоборот два потока в один схлопнуть?
Здравствуйте, Sharov, Вы писали:
S>Слишком гранулярная оптимизация для managed языков. Скорее всего я не прав, но моя точка зрения такая: вот мы оптимизируем на уровне линий кэша, вот у нас имеется сторонняя инфраструктура (jit+gc), нами не контролируемая.
Это _не_ оптимизация, это не пессимизация скорее.
Никакой магии тут нет. Точнее, не больше магии, чем в классике про branch prediction или в использовании быстрой сортировки вместо пузырьковой. Если грабли уже 100 раз хожены-перехожены, смысл ещё раз на них наступать?
S>А вот допустим мы закладываемся на spatial locality в нашем коде, не учитывая инфраструктуру.
Ну... закладываться-то можно, только её нужно ещё и обеспечить
Или храним данные в массиве, или вытаскиваем часть кода в unmanaged и рулим сами.
Как иначе-то?*
Здравствуйте, e.thrash, Вы писали:
ET>Здравствуйте, Sharov, Вы писали:
S>>Здравствуйте, Sinix, Вы писали:
S>>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю...
ET>jit может пустить расчет каждого свойства в отдельном потоке или наоборот два потока в один схлопнуть?
S>Или храним данные в массиве, или вытаскиваем часть кода в unmanaged и рулим сами.
Ситуация: храним данные в массиве, работаем с массивом и т.д. и т.п, т.е. spatial locality обеспечена. Во время манипуляций с массивом, начинает свою работу gc и содержимое кэша херится. Такое возможно?
Здравствуйте, Sharov, Вы писали:
S>Здравствуйте, e.thrash, Вы писали:
ET>>Здравствуйте, Sharov, Вы писали:
S>>>Здравствуйте, Sinix, Вы писали:
S>>>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю...
ET>>jit может пустить расчет каждого свойства в отдельном потоке или наоборот два потока в один схлопнуть?
S>Извините, но не понял вопрос.
изначально я спросил как распаралеллить расчет 2 непересекающихся свойств одного объекта, чтобы расчет был не 2 минуты, а одну.
вы написали S>>>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним.
Здравствуйте, Sharov, Вы писали:
S>Ситуация: храним данные в массиве, работаем с массивом и т.д. и т.п, т.е. spatial locality обеспечена. Во время манипуляций с массивом, начинает свою работу gc и содержимое кэша херится. Такое возможно?
Троллинг mode: c spatial locality всё по прежнему ок, в процессе массив переедет целиком (если переедет вообще). Вот с cache locality ой, да.
Короткий ответ: в этом случае замусоривание кэша — меньшая из ваших проблем.
Начнём с очевидного: полный stop the world — это очень больно, обидно и неактуально. <gcConcurrent> на свежих рантаймах по умолчанию выставлен в true, stop the world выполняется только для младших поколений, плюс, для эстетов завезли SustainedLowLatency. За подробностями — тынц и тынц.
Тем не менее, полная сборка мусора, даже в фоне и без блокировок — удовольствие всё равно дорогое и его следует по возможности избегать.
Хорошая новость: ситуация "работаем с массивом" редко приводит к вызову сборки мусора. Разве что вы аллоцируете память как не в себя. Но тогда, опять-таки, кэш — меньшая из ваших проблем
Здравствуйте, Sharov, Вы писали:
S>Я к тому, что оптимизация на таком уровне (линии кэша, кто кого вытолкнет и т.д.) особого смысла, кмк, не имеет.
Тут вопрос затраты/выигрыш. Ничего кроме знать и примерно представлять, как оно работает тут не требуется, так что можно и заморочиться. Ну да, это мелочи второго порядка, которые оказывают гораздо меньший эффект, чем, скажем, боксинг, но хорошо написанный код должен учитывать и эти моменты.
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, Sharov, Вы писали:
S>>Я к тому, что оптимизация на таком уровне (линии кэша, кто кого вытолкнет и т.д.) особого смысла, кмк, не имеет.
S>Тут вопрос затраты/выигрыш. Ничего кроме знать и примерно представлять, как оно работает тут не требуется, так что можно и заморочиться. Ну да, это мелочи второго порядка, которые оказывают гораздо меньший эффект, чем, скажем, боксинг, но хорошо написанный код должен учитывать и эти моменты.
Нет, это не мелочи для как минимум матричных и проч. мат операций, которые критичны к производительности. Но по отношению к другим возможным косякам, это да, мелочи. Т.е. все серьезные мат. библиотеки (от того же интела) писались и будут писаться на с\с++\фортран.
Здравствуйте, Sinix, Вы писали:
S>Здравствуйте, e.thrash, Вы писали:
ET>>Нужна ли в данном случае синхронизация?
S>Нет (при условии, что оба потока не разделяют друг с другом какие-либо изменяемые данные). S>Но есть нюансЪ: активная запись-чтение по соседним адресам приводит к накладным расходам на синхронизацию линеек кэша. S>Воспроизвести можно, но нужно постараться.
Гораздо вероятнее, что код в будущем изменится и неявное предположение, что параллельный код меняет разные части объекта, станет ложным.
Здравствуйте, e.thrash, Вы писали:
ET>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.
Это сейчас так, а как оно будет через полгода-год? Какой код гарантирует, что эти два метода не будут менять одни и те же данные? Или там просто камент с предупреждением висеть будет?
ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ? ET>Нужна ли в данном случае синхронизация?
Всё тайное нужно сделать явным. Разделить Car на два объекта, после вычислений собирать снова в один, например.
Здравствуйте, Vladek, Вы писали:
S>>Воспроизвести можно, но нужно постараться. V> Гораздо вероятнее, что код в будущем изменится и неявное предположение, что параллельный код меняет разные части объекта, станет ложным.
О, подход из серии "мы закладываемся на то, что этот объект никто не будет использовать"? Одобряю.
Если серьёзно, члены типа или поддерживают использование в нескольких потоках, или нет. Большинство instance members в BCL — нет, это соглашение по умолчанию.
Если вы решили отступить от этого контракта, ответственность "спроектировать тип так, чтобы при его использовании не возникало косяков" целиком и полностью ваша
ET>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car. ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ? ET>Нужна ли в данном случае синхронизация?
Если цель распараллелить длительные вычисления отдельных свойств, то лучше так и сделать, а сливать их и обновлять объект уже можно после того, как они вычислены.
Как уже заметили ниже, поскольку вычисляются два разных аспекта, то менять объек Car из разных потоков можно. Но ведь это может измениться (да, это размышления на будущее). Но поскольку разделить вычисления новых значений от изменения объекта car — задача тривиальная и в любом случае лучше разделяет ответственности в коде, то проще поступить так с самого начала: выделить вычисления в отдельные методы, запустить вычисления параллельно, а затем "заджоиниться" и обновить объект, когда обе операции будут завершены.