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