Паралелльное изменение одного объекта
От: e.thrash  
Дата: 09.04.16 07:08
Оценка:
Есть такой метод

Calculate(Car car)
{
   CalculatePrice(car);
   CalculateUseTime(car);
}



каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.
Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?
Нужна ли в данном случае синхронизация?
Re: Паралелльное изменение одного объекта
От: LaptevVV Россия  
Дата: 09.04.16 07:30
Оценка: -2
ET>
ET>Calculate(Car car)
ET>{
ET>   CalculatePrice(car);
ET>   CalculateUseTime(car);
ET>}
ET>

ET>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.
ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?
ET>Нужна ли в данном случае синхронизация?
Классика.
Первый метод прочитал car и начал работать.
Второй метод прочитал car и начал работать.
Первый метод записал измененный car.
Второй метод записал свой car — и стер все изменения, записанные первым методом.
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Re: Паралелльное изменение одного объекта
От: mogikanin Россия  
Дата: 09.04.16 08:00
Оценка:
Здравствуйте, e.thrash, Вы писали:

ET>Есть такой метод


ET>
ET>Calculate(Car car)
ET>{
ET>   CalculatePrice(car);
ET>   CalculateUseTime(car);
ET>}
ET>

ET>Нужна ли в данном случае синхронизация?

Нет

ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?


Неопасно, но не очевидно. Лучше сделать
car.Price =CalculatePrice()
Re[2]: Паралелльное изменение одного объекта
От: samius Япония http://sams-tricks.blogspot.com
Дата: 09.04.16 16:43
Оценка: +1
Здравствуйте, 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;
    }
Re: Паралелльное изменение одного объекта
От: Sinix  
Дата: 09.04.16 17:40
Оценка: 11 (2) +1
Здравствуйте, e.thrash, Вы писали:

ET>Нужна ли в данном случае синхронизация?


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

Во-первых, на современных ноутбуках надо включать режим "максимальная производительность" и запитывать от сети. Иначе один и тот же код при последовательных запусках выдаёт 5-15% разницы, если утыкается в CPU и до 40% — если в память. Разумеется, это для коротких бенчмарков, для длинных разница сглаживается.

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

В результате получаем нечто вроде
             SameInstance:  1506ms, ips:         331 983 907,28 | Mem:  31,81 kb, GC 0/1/2: 0/0/0 => 500000000
             TwoInstances:  1245ms, ips:         401 344 632,95 | Mem:  19,05 kb, GC 0/1/2: 0/0/0 => 500000000
         TwoInstancesNear:  1475ms, ips:         338 886 094,96 | Mem:  10,28 kb, GC 0/1/2: 0/0/0 => 500000000

(x64, релизная сборка, ctrl-F5).

  Пруфкод
        class SharedData
        {
            public const int Count = 500 * 1000 * 1000;
            public volatile int A;
            public volatile int B;
            public void InitA()
            {
                A = 0;
                for (int i = 0; i < Count; i++)
                {
                    A++;
                }
                A = 0;
            }
            public void InitB()
            {
                B = 0;
                for (int i = 0; i < Count; i++)
                {
                    B++;
                }
                B = 0;
            }
        }

        public static void Main(string[] args)
        {
            Console.WindowWidth = 120;

            // Warmup
            var c = new SharedData();
            c.InitA();
            c.InitB();
            c = null;

            // Allocated together
            var c1 = new SharedData();
            var c2 = new SharedData();

            // Allocated some pages later
            for (int i = 0; i < 10; i++)
            {
                var b = new byte[64 * 1024];
                b[b.Length - 1] = 1;
            }
            var c3 = new SharedData();

            Measure("SameInstance", () =>
            {
                var t1 = Task.Run(() => c1.InitA());
                var t2 = Task.Run(() => c1.InitB());
                Task.WhenAll(t1, t2).Wait();
                return SharedData.Count;
            });
            Measure("TwoInstances", () =>
            {
                var t1 = Task.Run(() => c2.InitA());
                var t2 = Task.Run(() => c3.InitB());
                Task.WhenAll(t1, t2).Wait();
                return SharedData.Count;
            });
            Measure("TwoInstancesNear", () =>
            {
                var t1 = Task.Run(() => c1.InitA());
                var t2 = Task.Run(() => c2.InitB());
                Task.WhenAll(t1, t2).Wait();
                return SharedData.Count;
            });

            Console.WriteLine("\r\nDone.");
            Console.ReadKey();
        }

        static void Measure(string name, Func<long> callback)
        {
            GC.Collect();
            GC.WaitForPendingFinalizers();
            GC.Collect();

            var mem = GC.GetTotalMemory(true);
            var gc00 = GC.CollectionCount(0);
            var gc01 = GC.CollectionCount(1);
            var gc02 = GC.CollectionCount(2);

            var sw = Stopwatch.StartNew();
            var result = callback();
            sw.Stop();

            var mem2 = GC.GetTotalMemory(false);
            var gc10 = GC.CollectionCount(0);
            var gc11 = GC.CollectionCount(1);
            var gc12 = GC.CollectionCount(2);

            var memDelta = (mem2 - mem) / 1024.0;
            var gcDelta0 = gc10 - gc00;
            var gcDelta1 = gc11 - gc01;
            var gcDelta2 = gc12 - gc02;

            Console.WriteLine(
                "{0,25}: {1,5}ms, ips: {2,22:N} | Mem: {3,6:N2} kb, GC 0/1/2: {4}/{5}/{6} => {7,6}",
                name, sw.ElapsedMilliseconds, result / sw.Elapsed.TotalSeconds, memDelta, gcDelta0, gcDelta1, gcDelta2, result);
        }


Подробности и более-менее стабильное воспроизведение см в отличной статье от И.Островского, раздел "Example 6: False cache line sharing".

P.S. Если кто-то будет принимать какие-то решения на основе этого поста, без замеров на реальном коде — вы знаете, кто сам себе злой буратина, ок?
Отредактировано 09.04.2016 19:49 Sinix . Предыдущая версия . Еще …
Отредактировано 09.04.2016 17:49 Sinix . Предыдущая версия .
Отредактировано 09.04.2016 17:48 Sinix . Предыдущая версия .
Отредактировано 09.04.2016 17:45 Sinix . Предыдущая версия .
Re[2]: Паралелльное изменение одного объекта
От: dr. Acula Украина  
Дата: 09.04.16 17:59
Оценка: +2 :)
ET>>
ET>>Calculate(Car car)
ET>>{
ET>>   CalculatePrice(car);
ET>>   CalculateUseTime(car);
ET>>}
ET>>

ET>>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.
ET>>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?
ET>>Нужна ли в данном случае синхронизация?
LVV>Классика.
Лебединое? Принц Датский?
LVV>Первый метод прочитал car и начал работать.
Отлично.
LVV>Второй метод прочитал car и начал работать.
Так-так.
LVV>Первый метод записал измененный car.
Какой изменённый?
LVV>Второй метод записал свой car — и стер все изменения, записанные первым методом.
Какой свой?

Профессор не слышал про указатели?
Или если нет звёздочки — то всё, копия?
Re[3]: Паралелльное изменение одного объекта
От: LaptevVV Россия  
Дата: 09.04.16 19:49
Оценка:
LVV>>Классика.
S>Что я делаю не так, почему не могу воспроизвести "классику"?

S>
S>    class Program
S>    {
S>        static void Main(string[] args)
S>        {
S>            const int iterationCount = 10000000;
S>            var car = new Car();

S>            var priceTask = Task.Run(
S>                () =>
S>                {
S>                    for (int i = 0; i < iterationCount; i++)
S>                        CalculatePrice(car);
S>                });
S>            var useTimeTask = Task.Run(
S>                () =>
S>                {
S>                    for (int i = 0; i < iterationCount; i++)
S>                        CalculateUseTime(car);
S>                });

S>            Task.WaitAll(priceTask, useTimeTask);
S>            Console.WriteLine(car.Price);
S>            Console.WriteLine(car.UseTime);
S>        }
S>        static void CalculatePrice(Car car)
S>        {
S>            car.Price += 1;
S>        }
S>        static void CalculateUseTime(Car car)
S>        {
S>            car.UseTime += 1;
S>        }
S>    }

S>    class Car
S>    {
S>        public int Price;
S>        public int UseTime;
S>    }

S>

Ну, век живи — век учись.
Это я про себя...
Это же Додиез.
А в нем — ссылки, а не значения...
Хочешь быть счастливым — будь им!
Без булдырабыз!!!
Отредактировано 09.04.2016 19:50 LaptevVV . Предыдущая версия .
Re[2]: Маленький оффтоп.
От: Sharov Россия  
Дата: 11.04.16 09:08
Оценка:
Здравствуйте, Sinix, Вы писали:

Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним. Или я чего-то не понимаю...
Кодом людям нужно помогать!
Re[3]: Маленький оффтоп.
От: Sinix  
Дата: 11.04.16 11:52
Оценка:
Здравствуйте, Sharov, Вы писали:

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

JIT довольно предсказуем. В смысле, оптимизации примерно одни и те же, что в 4.0 x86, что на свежем ryuJit. На старом x64 местами была беда, но сейчас про это можно забыть с чистой совестью.

Ну и знать — не значит "нужно обязательно использовать". Скорее наоборот, знать — это видеть косяки _до_ того, как они выстрелят. Как в этом топике или в примерах с интерполяцией строк
Автор: Sinix
Дата: 31.03.16
или с Convert.ChangeType
Автор: Sinix
Дата: 05.04.16
.
Re[4]: Маленький оффтоп.
От: Sharov Россия  
Дата: 11.04.16 12:29
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, Sharov, Вы писали:


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

S>JIT довольно предсказуем. В смысле, оптимизации примерно одни и те же, что в 4.0 x86, что на свежем ryuJit. На старом x64 местами была беда, но сейчас про это можно забыть с чистой совестью.

Слишком гранулярная оптимизация для managed языков. Скорее всего я не прав, но моя точка зрения такая: вот мы оптимизируем на уровне линий кэша, вот у нас имеется сторонняя инфраструктура (jit+gc), нами не контролируемая. Каждая из этих структур также работает с кэшом и проч. периферией. Jit более менее понятен, ибо один раз компилирует "раз и навсегда", т.е. temporal locality у нас не пострадает. А вот допустим мы закладываемся на spatial locality в нашем коде, не учитывая инфраструктуру. А тут бац, и гц начинает свою работу ломая все наши предположения об выполнении нашего кода и всяческих spatial locality. Такое возможно или я написал бред?
Кодом людям нужно помогать!
Re[3]: Маленький оффтоп.
От: e.thrash  
Дата: 11.04.16 12:53
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, Sinix, Вы писали:


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


jit может пустить расчет каждого свойства в отдельном потоке или наоборот два потока в один схлопнуть?
Re[5]: Маленький оффтоп.
От: Sinix  
Дата: 11.04.16 12:57
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Слишком гранулярная оптимизация для managed языков. Скорее всего я не прав, но моя точка зрения такая: вот мы оптимизируем на уровне линий кэша, вот у нас имеется сторонняя инфраструктура (jit+gc), нами не контролируемая.


Это _не_ оптимизация, это не пессимизация скорее.
Никакой магии тут нет. Точнее, не больше магии, чем в классике про branch prediction или в использовании быстрой сортировки вместо пузырьковой. Если грабли уже 100 раз хожены-перехожены, смысл ещё раз на них наступать?


S>А вот допустим мы закладываемся на spatial locality в нашем коде, не учитывая инфраструктуру.

Ну... закладываться-то можно, только её нужно ещё и обеспечить
Или храним данные в массиве, или вытаскиваем часть кода в unmanaged и рулим сами.
Как иначе-то?*

_____
* Разврат с fixed/GCHandle не предлагать.
Re[4]: Маленький оффтоп.
От: Sharov Россия  
Дата: 11.04.16 13:00
Оценка:
Здравствуйте, e.thrash, Вы писали:

ET>Здравствуйте, Sharov, Вы писали:


S>>Здравствуйте, Sinix, Вы писали:


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


ET>jit может пустить расчет каждого свойства в отдельном потоке или наоборот два потока в один схлопнуть?


Извините, но не понял вопрос.
Кодом людям нужно помогать!
Re[6]: Маленький оффтоп.
От: Sharov Россия  
Дата: 11.04.16 13:03
Оценка:
Здравствуйте, Sinix, Вы писали:


S>Или храним данные в массиве, или вытаскиваем часть кода в unmanaged и рулим сами.


Ситуация: храним данные в массиве, работаем с массивом и т.д. и т.п, т.е. spatial locality обеспечена. Во время манипуляций с массивом, начинает свою работу gc и содержимое кэша херится. Такое возможно?
Кодом людям нужно помогать!
Re[5]: Маленький оффтоп.
От: e.thrash  
Дата: 11.04.16 13:18
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Здравствуйте, e.thrash, Вы писали:


ET>>Здравствуйте, Sharov, Вы писали:


S>>>Здравствуйте, Sinix, Вы писали:


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


ET>>jit может пустить расчет каждого свойства в отдельном потоке или наоборот два потока в один схлопнуть?


S>Извините, но не понял вопрос.


изначально я спросил как распаралеллить расчет 2 непересекающихся свойств одного объекта, чтобы расчет был не 2 минуты, а одну.
вы написали
S>>>Никогда не понимал такой уровень оптимизации на managed языках. Все равно же то, что написано будет прогоняться через jit, и последнее слово за ним.
Re[7]: Маленький оффтоп.
От: Sinix  
Дата: 11.04.16 13:28
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Ситуация: храним данные в массиве, работаем с массивом и т.д. и т.п, т.е. spatial locality обеспечена. Во время манипуляций с массивом, начинает свою работу gc и содержимое кэша херится. Такое возможно?


Троллинг mode: c spatial locality всё по прежнему ок, в процессе массив переедет целиком (если переедет вообще). Вот с cache locality ой, да.


Короткий ответ: в этом случае замусоривание кэша — меньшая из ваших проблем.

Начнём с очевидного: полный stop the world — это очень больно, обидно и неактуально. <gcConcurrent> на свежих рантаймах по умолчанию выставлен в true, stop the world выполняется только для младших поколений, плюс, для эстетов завезли SustainedLowLatency. За подробностями — тынц и тынц.


Тем не менее, полная сборка мусора, даже в фоне и без блокировок — удовольствие всё равно дорогое и его следует по возможности избегать.
Хорошая новость: ситуация "работаем с массивом" редко приводит к вызову сборки мусора. Разве что вы аллоцируете память как не в себя. Но тогда, опять-таки, кэш — меньшая из ваших проблем
Отредактировано 11.04.2016 13:29 Sinix . Предыдущая версия .
Re[8]: Маленький оффтоп.
От: Sharov Россия  
Дата: 11.04.16 14:17
Оценка:
Здравствуйте, Sinix, Вы писали:


S>Троллинг mode: c spatial locality всё по прежнему ок, в процессе массив переедет целиком (если переедет вообще). Вот с cache locality ой, да.


QED

S>Короткий ответ: в этом случае замусоривание кэша — меньшая из ваших проблем.


Я к тому, что оптимизация на таком уровне (линии кэша, кто кого вытолкнет и т.д.) особого смысла, кмк, не имеет.
Кодом людям нужно помогать!
Re[9]: Маленький оффтоп.
От: Sinix  
Дата: 11.04.16 14:48
Оценка:
Здравствуйте, Sharov, Вы писали:

S>Я к тому, что оптимизация на таком уровне (линии кэша, кто кого вытолкнет и т.д.) особого смысла, кмк, не имеет.


Тут вопрос затраты/выигрыш. Ничего кроме знать и примерно представлять, как оно работает тут не требуется, так что можно и заморочиться. Ну да, это мелочи второго порядка, которые оказывают гораздо меньший эффект, чем, скажем, боксинг, но хорошо написанный код должен учитывать и эти моменты.
Re[10]: Маленький оффтоп.
От: Sharov Россия  
Дата: 11.04.16 15:01
Оценка: +1
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, Sharov, Вы писали:


S>>Я к тому, что оптимизация на таком уровне (линии кэша, кто кого вытолкнет и т.д.) особого смысла, кмк, не имеет.


S>Тут вопрос затраты/выигрыш. Ничего кроме знать и примерно представлять, как оно работает тут не требуется, так что можно и заморочиться. Ну да, это мелочи второго порядка, которые оказывают гораздо меньший эффект, чем, скажем, боксинг, но хорошо написанный код должен учитывать и эти моменты.


Нет, это не мелочи для как минимум матричных и проч. мат операций, которые критичны к производительности. Но по отношению к другим возможным косякам, это да, мелочи. Т.е. все серьезные мат. библиотеки (от того же интела) писались и будут писаться на с\с++\фортран.
Кодом людям нужно помогать!
Re[2]: Паралелльное изменение одного объекта
От: Vladek Россия Github
Дата: 12.04.16 09:48
Оценка:
Здравствуйте, Sinix, Вы писали:

S>Здравствуйте, e.thrash, Вы писали:


ET>>Нужна ли в данном случае синхронизация?


S>Нет (при условии, что оба потока не разделяют друг с другом какие-либо изменяемые данные).

S>Но есть нюансЪ: активная запись-чтение по соседним адресам приводит к накладным расходам на синхронизацию линеек кэша.
S>Воспроизвести можно, но нужно постараться.

Гораздо вероятнее, что код в будущем изменится и неявное предположение, что параллельный код меняет разные части объекта, станет ложным.
Re: Паралелльное изменение одного объекта
От: Vladek Россия Github
Дата: 12.04.16 09:54
Оценка:
Здравствуйте, e.thrash, Вы писали:

ET>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.


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

ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?

ET>Нужна ли в данном случае синхронизация?

Всё тайное нужно сделать явным. Разделить Car на два объекта, после вычислений собирать снова в один, например.
Отредактировано 12.04.2016 9:54 Vladek . Предыдущая версия .
Re[3]: Паралелльное изменение одного объекта
От: Sinix  
Дата: 12.04.16 10:00
Оценка:
Здравствуйте, Vladek, Вы писали:

S>>Воспроизвести можно, но нужно постараться.

V> Гораздо вероятнее, что код в будущем изменится и неявное предположение, что параллельный код меняет разные части объекта, станет ложным.

О, подход из серии "мы закладываемся на то, что этот объект никто не будет использовать"? Одобряю.

Если серьёзно, члены типа или поддерживают использование в нескольких потоках, или нет. Большинство instance members в BCL — нет, это соглашение по умолчанию.
Если вы решили отступить от этого контракта, ответственность "спроектировать тип так, чтобы при его использовании не возникало косяков" целиком и полностью ваша
Re: Паралелльное изменение одного объекта
От: SergeyT. США http://sergeyteplyakov.blogspot.com/
Дата: 12.04.16 16:05
Оценка: +2
Здравствуйте, e.thrash, Вы писали:


ET>каждый из методов вычисляет и меняет независимые друг от друга данные объекта Car.

ET>Насколько опасным будет код если эти два метода распаллелить в Task.ContinueWhenAll ?
ET>Нужна ли в данном случае синхронизация?

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

Вот псевдокод:


async Task Calculate(Car car)
{
   var priceTask = CalculatePriceAsync(inputForPrice);
   var useTimeTask = CalculateUseTime(inputForUseTime);
   await Task.WhenAll(priceTask, useTimeTask);

   car.Price = priceTask.Result;
   car.UseTime = useTimeTask.Result;
}


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