Информация об изменениях

Сообщение Re: Паралелльное изменение одного объекта от 09.04.2016 17:40

Изменено 09.04.2016 19:49 Sinix

Здравствуйте, 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. Если кто-то будет принимать какие-то решения на основе этого поста, без замеров на реальном коде — вы знаете, кто сам себе злой буратина, ок?
Re: Паралелльное изменение одного объекта
Здравствуйте, 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. Если кто-то будет принимать какие-то решения на основе этого поста, без замеров на реальном коде — вы знаете, кто сам себе злой буратина, ок?