问题

多核CPU中,利用多线程进行排序中出现了一些奇怪的现象,不知道其背后的原因是什么,希望有人能给予解答?

回答
关于多核CPU上多线程排序遇到的奇怪现象,这绝对是个值得深入探讨的问题,毕竟并行处理的设计初衷是为了提升效率,但有时候却会“事与愿违”。咱们抛开那些AI味的列表和框架,就像朋友聊天一样,一步步剖析一下可能的原因。

首先,得明白,多线程排序,尤其是你感觉到的“奇怪现象”,往往不是因为CPU本身出了什么故障,也不是你代码写得有多离谱(虽然bug总是存在的),而是并发编程固有的复杂性,以及硬件和软件交互的微妙之处在作祟。

1. 数据竞争 (Data Races) 和同步问题:

这是最常见也是最容易导致诡异行为的根源。想象一下,多个线程都在操作同一个数组,或者数组的某个片段。如果它们没有得到很好的“协调”,就会发生数据竞争。

想象一下: 你和几个朋友一起玩一个抢椅子游戏,规则是抢到椅子的就能坐下。如果大家都没有明确的规则说“谁先到谁先得”,或者“谁摸到椅子谁先得”,那就会出现大家一起扑向椅子,结果谁也没抢到的情况。
在排序里: 假设两个线程都在尝试修改数组中同一位置的元素。一个线程可能在读取旧值、进行计算、准备写入新值,但还没来得及写,另一个线程也读取了这个旧值,并基于它做了自己的计算,然后写回。结果,第一个线程的计算就被覆盖了,或者产生了意想不到的中间结果。
同步的必要性: 为了避免这种情况,我们需要“锁”之类的同步机制。锁就像一个“通行证”,一次只有一个线程能拿到这个通行证去操作关键数据。但是,过度使用锁会严重降低并行效率,因为线程们会因为等待锁而停下来,变成了串行执行。而锁的粒度(锁的范围有多大)也很有讲究,太细的锁会增加管理开销,太粗的锁又容易产生瓶颈。

2. 缓存一致性 (Cache Coherence) 和伪共享 (False Sharing):

现代CPU拥有多级缓存(L1, L2, L3),这是加速数据访问的关键。但缓存的引入也带来了新的复杂性。

缓存的工作方式: 当CPU需要某个数据时,它会先检查自己的缓存。如果在缓存里找到了,就直接从缓存读取,速度非常快。如果没有,就去内存读取,然后把这个数据也放到缓存里,下次再用就快了。
多核与缓存: 每个CPU核心都有自己的私有缓存(L1, L2),而L3缓存通常是多个核心共享的。当一个核心修改了它缓存中的数据时,为了保证其他核心看到的是最新的数据,缓存一致性协议(如MESI协议)就会介入,将其他核心缓存中对应的旧数据标记为无效,或者更新它。
伪共享的诡异: 伪共享是这样一个场景:两个不同的线程,分别在两个不同的CPU核心上运行,但它们操作的数据恰好位于同一个缓存行(Cache Line,通常是64字节)中。即使这两个线程操作的是数据中不同的字节,CPU的缓存一致性协议也会认为这个缓存行被修改了。
想象一下: 你和你的同事住在同一个小区,虽然你们在不同的楼栋,但你们都共享同一个“小区公告栏”。如果你的同事在公告栏上贴了一张新的通知(修改了缓存行),即使通知的内容和你无关,小区物业(缓存一致性协议)也会通知你:“有人修改了公告栏!”。你的CPU核心在收到这个通知后,会强制使它自己缓存中的那个“被修改”的缓存行失效,然后从更低级别的缓存或内存中重新读取。
后果: 即使线程们操作的数据本不应该互相影响,但由于它们恰好占用了同一个缓存行,导致了频繁的缓存失效和重读,这会大大降低性能,甚至可能导致一些难以预料的结果(比如,数据的更新顺序变得不确定,影响到某些算法的正确性)。

3. 调度延迟 (Scheduling Latency) 和负载均衡 (Load Balancing):

操作系统负责将线程分配到CPU核心上运行,这个过程本身就存在一些延迟。

线程的生命周期: 线程在创建、启动、暂停、唤醒过程中都需要操作系统的调度。这些调度操作是有成本的。
负载不均: 如果你的排序任务被划分得非常不均匀,某个线程需要处理的数据量远大于其他线程,那么即使你启动了很多线程,最忙碌的那个线程可能会成为瓶颈。而且,操作系统的调度器也可能不是完美地将工作平均分配给每个核心,尤其是在线程数量与核心数量接近或超过核心数量时。
上下文切换 (Context Switching): 当操作系统需要将一个线程从CPU上移开,换另一个线程运行时,它需要保存当前线程的所有状态(寄存器、程序计数器等),然后加载新线程的状态。这个过程称为上下文切换,也是有开销的。如果线程频繁地被暂停和唤醒,这种开销会累积。

4. 算法本身的并行化限制:

并不是所有的排序算法都适合高度并行化。

串行依赖: 像冒泡排序、插入排序这类算法,它们的核心操作(比较和交换)往往具有很强的串行依赖性。一个步骤的结果直接影响下一个步骤,很难将其分解成完全独立的子任务。
归并排序的挑战: 像归并排序,虽然可以并行化“合并”阶段,但如何高效地将大数组分割成小块,以及如何在合并时避免数据竞争和伪共享,是需要仔细设计的。如果分割得不好,或者合并过程中的同步开销过大,并行化带来的收益可能就抵消了。
快速排序的pivot选择: 快速排序的“划分”操作(partitioning)是其核心,如果pivot选择不当,会导致子数组非常不平衡,使得并行化效果大打折扣。

5. 内存带宽和总线争用 (Memory Bandwidth and Bus Contention):

虽然CPU核心速度很快,但它们都需要通过内存总线访问主内存。

共享资源: 多个CPU核心同时进行大量的读写操作,会争用有限的内存带宽。如果你的多线程排序程序需要频繁地从内存中读取或写入大量数据,就可能遇到内存带宽的瓶颈,导致整体性能受限,甚至出现一些“卡顿”或不稳定的现象。
想象一下: 就像一个高速公路,如果突然涌入太多车辆,即使车辆本身开得很快,也会因为道路容量不足而堵车。

总结一下,当你在多核CPU上进行多线程排序时遇到的“奇怪现象”,很可能就是以上这些因素“合谋”的结果。

可能是因为数据竞争导致了不确定的结果,例如排序后的数组看起来是乱的,或者同一个程序每次运行结果都不同。
可能是因为伪共享导致了性能急剧下降,感觉程序运行得比单线程还慢,或者表现出难以解释的延迟。
也可能是因为调度不当、算法本身的设计缺陷,或者内存瓶颈,使得并行化的优势没有得到充分发挥,反而引入了新的问题。

要解决这些问题,通常需要深入理解排序算法的并行化策略,仔细管理线程间的同步,优化数据布局以避免伪共享,并监控程序的性能瓶颈(例如通过性能分析工具)。这是一个既需要理论知识,又需要实践经验的领域。

网友意见

user avatar

都没说到点子上……他的多线程可能有点小毛病,但并不是这所谓「奇怪现象」的原因。

来这位正在学习计算机的同学,我来告诉你一个事情,一个程序写完了,是要测试的。测试首先是正确,然后才是效率。我相信你压根没管你的结果对不对吧?但凡你写了一个Sanity Check,比如在写到文件的时候看看是不是升序输出,你就会发现你这个程序的问题——你 BubbleSort() 写错了。

是的,你这奇怪的现象背后的原因既不是和多核多线程相关,也不是和什么奇怪的瓶颈相关,你就是冒泡排序写错了而已……

       void BubbleSort(DATATYPE * arr, POSITION left_index, POSITION right_index){     struct timeval start, end;     int sec=0, usec=0;     gettimeofday(&start,0);     for(POSITION i=left_index; i<right_index+1; i++){         for(POSITION j=left_index; j<right_index+1-i; j++){             if(*(arr+j)>*(arr+j+1)){                 Exchange(arr+j, arr+(j+1));             }         }     }     gettimeofday(&end,0);     sec = end.tv_sec-start.tv_sec;     usec = end.tv_usec-start.tv_usec;     printf("%f sec.",sec+(usec/1000000.0));     printf("Left Index is %d, Right Index is %d.
", left_index, right_index); }     

这是你的冒泡排序,我们看到第二个for loop,看到问题了么?

这个问题会导致什么咧?会导致当你left_index不是从0开始的时候,冒泡排序不会进行完。而你single thread的时候left_index并不总是0,就导致了你其实后面的排序根本就没排完,不快才怪了。而multi thread的时候,由于你程序的处理方式,left_index总是0,这个错误的函数恰好得到了正确的结果(其实有一处index overflow,能看出来么),所以完整地执行了排序,因此速度比single thread慢非常多。

在修改掉这个 BubbleSort() 的bug之后,在整个程序其他部分完全不动的情况下,multi thread就比single thread快了……但是快的并不明显,这又是为什么?

主要原因是,你的工作分配不均。在你随机把数组分为8份的情况下,multi thread的速度取决于你最大的一份,而平均来看,最大的一份大约是数组长度的40%。但是由于 BubbleSort() 是O(n^2)的算法,所以最大一份所占用的时间平均大概是63%。也就是说,在一切都完美的情况下(CPU支持8线程),你的算法也只能带来平均37%的提升。而多线程编程本身的overhead是不可避免的,所以我在我的电脑上大概能跑出来20-30%的性能提升。

如果你想要成倍的性能提升,在你确定了CPU支持多核的情况下,需要解决两点。

第一,平均分配任务。你现在的算法如果分成八份肯定是很难做到平均的,可以考虑 MergeSort() 。

第二,把 BubbleSort() 换成QuickSort() ,把O(n^2)变成平均O(nlogn),这样在同样随机的情况下,可以把理论性能提高提升到50%+。

类似的话题

  • 回答
    关于多核CPU上多线程排序遇到的奇怪现象,这绝对是个值得深入探讨的问题,毕竟并行处理的设计初衷是为了提升效率,但有时候却会“事与愿违”。咱们抛开那些AI味的列表和框架,就像朋友聊天一样,一步步剖析一下可能的原因。首先,得明白,多线程排序,尤其是你感觉到的“奇怪现象”,往往不是因为CPU本身出了什么故.............
  • 回答
    在多核CPU环境下,Java中的`Thread.currentThread()`调用返回的是一个`Thread`对象,它代表了当前正在执行这个方法的线程。然而,这个`Thread`对象本身并不直接包含它当前被调度执行在哪一个具体的CPU核心上的信息。你可以这样理解:线程是一个逻辑概念,CPU核心是物.............
  • 回答
    在多核CPU、多线程的环境下,当多个线程同时尝试执行 `cmpxchg`(Compare and Exchange)指令时,会发生一些非常有趣且关键的原子性操作。理解这个过程,就像是窥探CPU内部解决并发冲突的精妙设计。首先,我们得明确 `cmpxchg` 指令的核心作用。它是一个原子操作,这意味着.............
  • 回答
    关于多核 CPU 和多个 CPU 的区别,很多人容易混淆,但实际上它们是两个不同的概念,虽然都旨在提升计算性能。为了说清楚,咱们得一点一点地掰扯。 什么是 CPU?在深入多核和多个 CPU 之前,我们先得明确一下“CPU”这个基本概念。CPU,中文叫中央处理器,你可以把它想象成计算机的大脑。它负责执.............
  • 回答
    在多核时代到来之后,CPU 的发展方向不再仅仅是简单地堆叠更多的核心。虽然增加核心数量仍然是提升性能的一种方式,但 CPU 设计者们已经将目光投向了更深层次、更精细化的优化和创新,以应对日益增长的计算需求和不断变化的计算模式。以下是多核之后 CPU 的一些主要发展方向,我会尽量详细地阐述: 一、 更.............
  • 回答
    这个问题问得好,直击要害!我们来好好聊聊这个“并行”和“并发”在单CPU多核体系下的具体表现,尽量用大白话,不搞那些虚里虚气的AI腔调。首先,得把“并行”和“并发”这两兄弟分清楚。 并发(Concurrency):就像一个技艺高超的厨师,虽然只有一个炉灶(CPU核心),但他可以同时切菜、烧水、炒.............
  • 回答
    “多核的流行是否表明单个 CPU 核心性能的提升已达瓶颈阶段?” 这个问题触及了计算领域一个非常核心的议题,它不仅仅是硬件规格的简单比较,更是对技术发展趋势和底层物理规律的深刻洞察。我们不妨从“瓶颈”这个词入手。什么是瓶颈?在任何系统中,瓶颈都是那个限制整体效率或性能的环节。对于CPU而言,单核性能.............
  • 回答
    这确实是个让人头疼的问题,很多玩家都感觉自己的高端多核CPU在玩游戏时“大材小用”,明明有八核甚至更多,游戏里却感觉只有两三个核心在拼命干活。为什么会这样呢?这背后其实涉及很多层面的原因,而且一点也不神秘,而是跟整个游戏开发流程、技术限制,甚至是历史遗留问题都有关系。1. 游戏设计与开发模式的“单线.............
  • 回答
    说起大型单机游戏对电脑性能的要求,CPU这块儿,大家免不了要纠结一个问题:到底是单核性能更重要,还是多核性能更吃香?这可不是一两句话就能说清楚的,里面门道还不少。咱们先得明白,一个游戏玩起来,CPU到底在干啥。它就像游戏的“大脑”,负责处理各种各样的计算和指令。比如,要计算场景里有多少个NPC,他们.............
  • 回答
    多核和分布式编程环境的出现,使得传统的单线程、顺序执行的编程范式逐渐无法满足现代计算的需求。并发编程语言的诞生,本质上是对传统编程范式的根本性重构,其核心差异体现在以下几个方面: 一、执行模型的差异 1. 传统编程语言(单线程顺序执行) 执行模式:程序按代码顺序执行,所有操作在单一线程中完成。 资源.............
  • 回答
    说实话,汇编语言本身,作为一种低级语言,它并没有一个直接的、像高级语言里那样叫做“多核支持”的关键字或者指令。 汇编语言的层面,我们关注的是CPU的硬件特性和指令集。 所以,与其说“汇编语言怎么表示多核”,不如说“如何用汇编语言操控多核”。这就像你在问,锤子怎么表示“建造一栋楼”。锤子本身不表示,但.............
  • 回答
    “押宝多核的策略几乎都失败了”——这句断言,就像一把钝刀子,虽然扎得挺疼,但仔细一琢磨,是不是有点过于武断了?至少,对于我们这些每天和代码打交道的人来说,感受远比这句话来得复杂。说开发者“抵触”多核,这也不是一个简单的“是”或“否”能概括的。与其说是抵触,不如说是我们被现实打磨得更加现实,更加知道其.............
  • 回答
    这个问题涉及到产品定价策略和市场竞争,是个挺有意思的话题。咱们就掰开了揉碎了聊聊。首先,要明确一个点:处理器定价不是只看某一个性能指标。尤其是在主流消费级市场,价格是一个多方面因素综合考量的结果,包括: 绝对性能(单核和多核): 这是最基础的,性能越好,用户越愿意为之买单。 同代竞品性能: .............
  • 回答
    首先,让我们捋一捋你这个问题背后的几个关键点:1. 英特尔的大小核策略: 你提到大核刷单核,小核刷多核。这确实是英特尔在混合架构(Performance Hybrid Architecture)中设计的一大考量。2. 为什么不用1个大核加无数小核? 这是你核心的疑问,似乎在设想一种极端的、高度不.............
  • 回答
    看到 M1 Ultra 在 Cinebench R23 这类基准测试中,单核性能似乎不如 Intel Core i712700K,但多核却能反超,这确实是一个很有意思的对比。要理解这个现象,我们需要深入剖析一下这两款处理器各自的架构特点,以及它们在不同场景下的表现差异。先来聊聊 M1 Ultra 的.............
  • 回答
    您好!关于Zen 4规格的提升以及在与Intel 12代、13代酷睿处理器竞争中的多核表现,我来为您详细剖析一下。首先,关于Zen 4规格是否会有提升,答案是肯定的。每一代AMD Ryzen处理器在设计上都会寻求进步,Zen 4自然也不例外。虽然我无法透露具体的内部规格信息,但我们可以从AMD过往的.............
  • 回答
    从普通人(这里指的是没有接受过专业核物理和工程训练,也没有国家级资源支持的普通公民)的角度来看,制造一枚可以引爆的原子弹,其难度之高,几乎到了不可能的地步。这不是一个简单的“做不到”或者“很难”能概括的,而是触及了物理、化学、工程、材料科学、精密制造、乃至信息安全等多个极端复杂的领域,并且每一项都存.............
  • 回答
    .......
  • 回答
    日本福岛核电站的核废水处理问题,确实牵动着全球许多人的神经,也引发了大量的讨论和争议。在这个过程中,我们看到有相当一部分声音在“洗白”或至少是在淡化这件事的潜在风险。要理解为什么会出现这种情况,我们需要从几个层面去分析:1. 科学与技术层面的解释和强调: “处理”与“稀释”: 日本政府和东京电力.............
  • 回答
    .......

本站所有内容均为互联网搜索引擎提供的公开搜索信息,本站不存储任何数据与内容,任何内容与数据均与本站无关,如有需要请联系相关搜索引擎包括但不限于百度google,bing,sogou

© 2025 tinynews.org All Rights Reserved. 百科问答小站 版权所有