Паралелльное изменение одного объекта
От: 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>Воспроизвести можно, но нужно постараться.

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