3

我通过设置每隔一个位为假的位数组进行枚举。

现在我想通过将它分成两个线程来加快速度。但是由于一些奇怪的原因,每个线程完成一半工作的时间需要多出 64% 的时间,我想知道为什么会这样?

这可能是由于某种 CPU 缓存效应造成的吗?我该如何正确地做到这一点?

我以前也用 lambda 表达式尝试过 8 个线程,但它总是在 ~1400 毫秒左右,但是在单线程中我总是得到 850 毫秒。此外,当我让一个线程完成所有工作时,我花了 830 毫秒。我就是不明白,有人知道这是什么原因吗?

代码:

    class Program
    {
        static int count = 0x10000000;
        static int half = count / 2;
        static BitArray bitArray = new BitArray(count);

        static unsafe void Main(string[] args)
        {
            Stopwatch sw = Stopwatch.StartNew();

#if SINGLE
            for (int i = 0; i < bitArray.Count; i += 2)
                bitArray.Set(i, true);
#else
            Thread thread1 = new Thread(Thread1);
            Thread thread2 = new Thread(Thread2);
            thread1.Start();
            thread2.Start();
            thread1.Join();
            thread2.Join();
#endif
            sw.Stop();

            Console.WriteLine(sw.ElapsedMilliseconds);
            Console.ReadLine();
        }

        static void Thread1()
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = 0; i < half; i += 2)
                bitArray.Set(i, true);
            sw.Stop();
            Console.WriteLine("Thread1: {0}", sw.ElapsedMilliseconds);
        }

        static void Thread2()
        {
            Stopwatch sw = Stopwatch.StartNew();
            for (int i = half; i < count; i += 2)
                bitArray.Set(i, true);
            sw.Stop();
            Console.WriteLine("Thread2: {0}", sw.ElapsedMilliseconds);
        }
    }
4

3 回答 3

9

BitArray 不是线程安全的类。你不应该那样使用它。事实上,除了正确性之外,这很可能是缓慢的原因。原因如下:

如果您查看 BitArray 的源代码,它包含一个int version字段,该字段在每次操作时都会更新,尤其是Set()您调用的 .

这意味着每个线程不断更新相同的内存位置,这是一个巨大的性能杀手,因为所有内核在访问该位置时都必须进行通信和同步。在这种情况下,多线程解决方案的性能比单核解决方案更差是完全合理的。

于 2013-08-08T00:47:30.237 回答
2

因为线程并不像看起来那么容易。

首先,如文档所述,BitArrays 不是线程安全的。这意味着当多个线程同时使用它们时,它们可能并且将会表现出不可预测的行为。

至于性能损失,可能是由于您的两个线程试图同时修改相同的内存位置,从而不断地使彼此的缓存无效造成的总线流量增加。

即使您认为您的线程没有修改相同的位置,这也可能不是真的。例如,BitArray 有一个Count属性。很有可能,每次您将位设置为 1 时,线程都会修改一个计数器变量,即支持该Count属性。由于竞争条件和陈旧值,这种并发修改是危险的,并且可能会增加总线流量,如我之前所述。

问题是 CPU 内核工作在 2-3GHz,而 RAM 和内存总线工作在 1Ghz。内存要慢得多,访问内存需要大约 100 个 CPU 周期。如果打破 CPU 的缓存机制,性能会明显下降。

编辑:更不用说线程创建和加入涉及大量开销。如果你的工作是 830 毫秒一次性。您不太可能通过多线程获得显着改进。您还应该尝试摆脱线程中的秒表,因为它们也是开销。

于 2013-08-08T00:45:02.803 回答
1

我修改了您的代码,以便测试运行 10 次并报告结果。使用您的代码,我看到单线程与多线程测试的时间相似(每个线程大约需要 1200 毫秒)。

但是,正如其他人所说,不能保证您在多个线程中使用单个 BitArray 不会导致线程之间的争用。

最简单的方法是为每个线程提供自己的 BitArray,而不是使用共享的静态 BitArray。使用这种方法,我通常会看到每个线程花费大约 450 毫秒,尽管偶尔仍会看到更长的时间:

Thread2: 415
Thread1: 420
447
Thread2: 414
Thread1: 420
496
Thread1: 1185
Thread2: 1198
1249
Thread1: 417
Thread2: 421
455
Thread1: 420
Thread2: 415
455
Thread2: 413
Thread1: 417
491
Thread2: 413
Thread1: 417
508
Thread2: 417
Thread1: 441
526
Thread1: 420
Thread2: 415
465
Thread1: 940
Thread2: 1005
1087

最终,我认为这表明:

  • 尽管进行了代码设计,但线程之间的 BitArray 仍然存在争用效果
  • 即使每个线程都有单独的位数组,代码的时序仍然存在“随机”效应,这表明通过像这样的微基准测试,您总是有效地进行基准测试,而不仅仅是您编写的代码。您还会受到 GC、CPU 缓存、上下文切换、核心跳跃、秒表不准确等的影响。
  • 如果您尝试编写的代码的真正目的是尽可能快地填充位数组,那么您可能需要一种更接近线路、更手动的方法,可能是另一种语言。
于 2013-08-08T01:21:35.887 回答