热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

为什么1000个线程比几个快?

如何解决《为什么1000个线程比几个快?》经验,为你挑选了2个好方法。

我有一个简单的程序,可以在2D点阵列中进行线性搜索.我对1000万个点进行1000次搜索.

奇怪的是,如果我生成1000个线程,程序的工作速度就像我只有我的CPU核心,或者当我使用Parallel.For时一样快.这与我所知道的关于创建线程的所有内容相反.创建和销毁线程很昂贵,但显然不是这种情况.

有人可以解释原因吗?

注意:这是一个方法论的例子; 搜索算法故意不意味着做到最优.重点是线程.

注2:我测试了4核i7和3核AMD,结果遵循相同的模式!

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Threading;

/// 
/// We search for closest points.
/// For every point in array searchData, we search into inputData for the closest point, 
/// and store it at the same position into array resultData;
/// 
class Program
{
    class Point
    {
        public double X { get; set; }
        public double Y { get; set; }

        public double GetDistanceFrom (Point p)
        {
            double dx, dy;
            dx = p.X - X;
            dy = p.Y - Y;
            return Math.Sqrt(dx * dx + dy * dy);
        }
    }

    const int inputDataSize = 1_000_000;
    static Point[] inputData = new Point[inputDataSize];

    const int searchDataSize = 1000;
    static Point[] searchData = new Point[searchDataSize];
    static Point[] resultData = new Point[searchDataSize];

    static void GenerateRandomData (Point[] array)
    {
        Random rand = new Random();
        for (int i = 0; i  threads = new List();
        for (int i = 0; i 
                {
                    int index = (int)obj;
                    SearchOne(index);
                });
            thread.Start(i);
            threads.Add(thread);
        }
        foreach (var t in threads) t.Join();
    }

    static void FewThreadSearch()
    {
        int threadCount = Environment.ProcessorCount;
        int workSize = searchDataSize / threadCount;
        List threads = new List();
        for (int i = 0; i 
                {
                    int[] range = (int[])obj;
                    int from = range[0];
                    int to = range[1];
                    for (int index = from; index 
                {
                    SearchOne(index);
                });
    }

    static void Main(string[] args)
    {
        Console.Write("Generatic data...  ");
        GenerateRandomData(inputData);
        GenerateRandomData(searchData);
        Console.WriteLine("Done.");
        Console.WriteLine();

        Stopwatch watch = new Stopwatch();

        Console.Write("All thread searching... ");
        watch.Restart();
        AllThreadSearch();
        watch.Stop();
        Console.WriteLine($"Done in {watch.ElapsedMilliseconds} ms.");

        Console.Write("Few thread searching... ");
        watch.Restart();
        FewThreadSearch();
        watch.Stop();
        Console.WriteLine($"Done in {watch.ElapsedMilliseconds} ms.");

        Console.Write("Parallel thread searching... ");
        watch.Restart();
        ParallelThreadSearch();
        watch.Stop();
        Console.WriteLine($"Done in {watch.ElapsedMilliseconds} ms.");

        Console.WriteLine();
        Console.WriteLine("Press ENTER to quit.");
        Console.ReadLine();
    }
}

编辑:请确保在调试器外运行应用程序.VS Debugger减慢了多线程的情况.


编辑2:更多测试.

要清楚,这里是更新的代码,保证我们做的有1000个运行在一次:

public static void AllThreadSearch()
{
    ManualResetEvent startEvent = new ManualResetEvent(false);
    List threads = new List();
    for (int i = 0; i 
        {
            startEvent.WaitOne();
            int index = (int)obj;
            SearchOne(index);
        });
        thread.Start(i);
        threads.Add(thread);
    }
    startEvent.Set();
    foreach (var t in threads) t.Join();
}

使用较小的阵列(100K元素)进行测试,结果如下:

1000 vs 8个主题

               Method |     Mean |    Error |    StdDev | Scaled |
--------------------- |---------:|---------:|----------:|-------:|
      AllThreadSearch | 323.0 ms | 7.307 ms | 21.546 ms |   1.00 |
      FewThreadSearch | 164.9 ms | 3.311 ms |  5.251 ms |   1.00 |
 ParallelThreadSearch | 141.3 ms | 1.503 ms |  1.406 ms |   1.00 |

现在,正如预期的那样,1000个线程要慢得多.Parallel.For仍然是他们所有人,这也是合乎逻辑的.

但是,将数组增长到500K(即每个线程的工作量),事情开始看起来很奇怪:

1000 vs 8,500K

               Method |     Mean |    Error |   StdDev | Scaled |
--------------------- |---------:|---------:|---------:|-------:|
      AllThreadSearch | 890.9 ms | 17.74 ms | 30.61 ms |   1.00 |
      FewThreadSearch | 712.0 ms | 13.97 ms | 20.91 ms |   1.00 |
 ParallelThreadSearch | 714.5 ms | 13.75 ms | 12.19 ms |   1.00 |

看起来上下文切换的成本可以忽略不计.线程创建成本也相对较小.拥有太多线程的唯一重要成本是内存丢失(内存地址).仅此一项就足够了.

现在,线程创建成本确实不大吗?我们普遍被告知创建线程非常糟糕,上下文切换是邪恶的.



1> open-collar..:

您可能想要考虑应用程序如何访问内存.在最大线程场景中,您有效地按顺序访问内存,从缓存的角度来看这是有效的.使用少量线程的方法更随机,导致缓存未命中.根据CPU的不同,还有一些性能计数器可用于测量L1和L2缓存命中/未命中.



2> pere57..:

我认为线程太多的真正问题(除了内存使用)是CPU可能很难优化自己,因为它一直在切换任务.在OP的原始基准测试中,线程都在处理相同的任务,因此您没有看到额外线程的大部分成本.

为了模拟处理不同任务的线程,我修改了Jodrell 对原始代码的重新构造(在下面的数据中标记为"Normal"),首先通过确保所有线程同时在同一块内存中工作来优化内存访问.该块使用此缓存阻止技术文章中的方法适合缓存(4mb).然后我"反转"它以确保每组4个线程在不同的内存块中工作.我的机器的结果(毫秒):

Intel Core i7-5500U CPU 2.40GHz (Max: 2.39GHz) (Broadwell), 1 CPU, 4 logical and 2 physical cores)
inputDataSize = 1_000_000; searchDataSize = 1000; blocks used for O/D: 10

Threads         1       2       3       4       6       8       10      18      32      56      100     178     316     562     1000
Normal(N)       5722    3729    3599    2909    3485    3109    3422    3385    3138    3220    3196    3216    3061    3012    3121
Optimized(O)    5480    2903    2978    2791    2826    2806    2820    2796    2778    2775    2775    2805    2833    2866    2988
De-optimized(D) 5455    3373    3730    3849    3409    3350    3297    3335    3365    3406    3455    3553    3692    3719    3900

对于O,所有线程同时在可缓存内存的同一块中工作(其中1个块= 1/10 inputData).对于D,对于每组4个线程,没有线程同时在同一块内存中工作.所以基本上,在前一种情况下,访问inputData能够使用缓存,而在后一种情况下,4线程访问inputData被强制使用主内存.

在图表中更容易看到.这些图表减去了线程创建成本,并注意x轴是对数的,y轴被截断以更好地显示数据的形状.此外,1个线程的值已减半,以显示理论上最佳的多线程性能:

综合结果 正常数据 优化数据 去优化数据

快速浏览一下就可以看出优化数据(O)确实比其他数据更快.它也更加一致(更平滑),因为与N相比,它不必处理缓存未命中.正如Jodrell所建议的那样,在100个线程附近似乎有一个最佳点,这是我的系统上允许线程在1个时间片内完成其工作的数字.之后,时间随线程数呈线性增加(请记住,x轴在图表上有对数刻度.)

比较正常和优化数据,前者是相当锯齿状的,而后者是平滑的.这个答案表明,从内存访问可能更"随机"的更少线程来看,从缓存的角度来看,更多线程会更有效.下面的图表似乎证实了这一点(注意4个线程对我的机器来说是最佳的,因为它有4个逻辑核心):

没有

去优化版本最有趣.最糟糕的情况是有4个线程,因为它们被迫在不同的内存区域工作,阻止了有效的缓存.随着线程数量的增加,系统能够在线程共享内存块时进行缓存.但是,随着线程数量的增加,大概是上下文切换使得系统再次高速缓存并且结果趋向于最坏情况:

做

我认为最后一张图表显示了上下文切换的实际成本.在原始(N)版本中,线程都在执行相同的任务.因此,除了CPU时间之外,对资源的竞争有限,并且CPU能够针对工作负载优化自身(即有效地缓存).如果线程都在做不同的事情,那么CPU无法优化自身和严重的性能打击结果.因此,不是直接导致问题的上下文切换,而是资源的竞争.

在这种情况下,O(2909ms)和D(3849ms)之间的4个线程的差异是940ms.这表示性能下降了32%.因为我的机器有一个共享的L3缓存,即使只有4个线程,这个性能也会出现.


推荐阅读
  • 前景:当UI一个查询条件为多项选择,或录入多个条件的时候,比如查询所有名称里面包含以下动态条件,需要模糊查询里面每一项时比如是这样一个数组条件:newstring[]{兴业银行, ... [详细]
  • 第四章高阶函数(参数传递、高阶函数、lambda表达式)(python进阶)的讲解和应用
    本文主要讲解了第四章高阶函数(参数传递、高阶函数、lambda表达式)的相关知识,包括函数参数传递机制和赋值机制、引用传递的概念和应用、默认参数的定义和使用等内容。同时介绍了高阶函数和lambda表达式的概念,并给出了一些实例代码进行演示。对于想要进一步提升python编程能力的读者来说,本文将是一个不错的学习资料。 ... [详细]
  • 模板引擎StringTemplate的使用方法和特点
    本文介绍了模板引擎StringTemplate的使用方法和特点,包括强制Model和View的分离、Lazy-Evaluation、Recursive enable等。同时,还介绍了StringTemplate语法中的属性和普通字符的使用方法,并提供了向模板填充属性的示例代码。 ... [详细]
  • 基于Socket的多个客户端之间的聊天功能实现方法
    本文介绍了基于Socket的多个客户端之间实现聊天功能的方法,包括服务器端的实现和客户端的实现。服务器端通过每个用户的输出流向特定用户发送消息,而客户端通过输入流接收消息。同时,还介绍了相关的实体类和Socket的基本概念。 ... [详细]
  • 本文详细介绍了Spring的JdbcTemplate的使用方法,包括执行存储过程、存储函数的call()方法,执行任何SQL语句的execute()方法,单个更新和批量更新的update()和batchUpdate()方法,以及单查和列表查询的query()和queryForXXX()方法。提供了经过测试的API供使用。 ... [详细]
  • [大整数乘法] java代码实现
    本文介绍了使用java代码实现大整数乘法的过程,同时也涉及到大整数加法和大整数减法的计算方法。通过分治算法来提高计算效率,并对算法的时间复杂度进行了研究。详细代码实现请参考文章链接。 ... [详细]
  • 摘要: 在测试数据中,生成中文姓名是一个常见的需求。本文介绍了使用C#编写的随机生成中文姓名的方法,并分享了相关代码。作者欢迎读者提出意见和建议。 ... [详细]
  • 本文讨论了Kotlin中扩展函数的一些惯用用法以及其合理性。作者认为在某些情况下,定义扩展函数没有意义,但官方的编码约定支持这种方式。文章还介绍了在类之外定义扩展函数的具体用法,并讨论了避免使用扩展函数的边缘情况。作者提出了对于扩展函数的合理性的质疑,并给出了自己的反驳。最后,文章强调了在编写Kotlin代码时可以自由地使用扩展函数的重要性。 ... [详细]
  • 本文讨论了一个数列求和问题,该数列按照一定规律生成。通过观察数列的规律,我们可以得出求解该问题的算法。具体算法为计算前n项i*f[i]的和,其中f[i]表示数列中有i个数字。根据参考的思路,我们可以将算法的时间复杂度控制在O(n),即计算到5e5即可满足1e9的要求。 ... [详细]
  • This article discusses the efficiency of using char str[] and char *str and whether there is any reason to prefer one over the other. It explains the difference between the two and provides an example to illustrate their usage. ... [详细]
  • 本文讨论了编写可保护的代码的重要性,包括提高代码的可读性、可调试性和直观性。同时介绍了优化代码的方法,如代码格式化、解释函数和提炼函数等。还提到了一些常见的坏代码味道,如不规范的命名、重复代码、过长的函数和参数列表等。最后,介绍了如何处理数据泥团和进行函数重构,以提高代码质量和可维护性。 ... [详细]
  • Android自定义控件绘图篇之Paint函数大汇总
    本文介绍了Android自定义控件绘图篇中的Paint函数大汇总,包括重置画笔、设置颜色、设置透明度、设置样式、设置宽度、设置抗锯齿等功能。通过学习这些函数,可以更好地掌握Paint的用法。 ... [详细]
  • 上图是InnoDB存储引擎的结构。1、缓冲池InnoDB存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。因此可以看作是基于磁盘的数据库系统。在数据库系统中,由于CPU速度 ... [详细]
  • HashMap的相关问题及其底层数据结构和操作流程
    本文介绍了关于HashMap的相关问题,包括其底层数据结构、JDK1.7和JDK1.8的差异、红黑树的使用、扩容和树化的条件、退化为链表的情况、索引的计算方法、hashcode和hash()方法的作用、数组容量的选择、Put方法的流程以及并发问题下的操作。文章还提到了扩容死链和数据错乱的问题,并探讨了key的设计要求。对于对Java面试中的HashMap问题感兴趣的读者,本文将为您提供一些有用的技术和经验。 ... [详细]
  • 超级简单加解密工具的方案和功能
    本文介绍了一个超级简单的加解密工具的方案和功能。该工具可以读取文件头,并根据特定长度进行加密,加密后将加密部分写入源文件。同时,该工具也支持解密操作。加密和解密过程是可逆的。本文还提到了一些相关的功能和使用方法,并给出了Python代码示例。 ... [详细]
author-avatar
高--洁
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有