You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

多线程性能逊于单线程?原因及优化方案探讨

多线程实现性能劣于单线程的原因及优化方案

问题背景

应用的核心性能热点是Panorama函数,其代码如下:

function Panorama(var Solutions: TSolutionDynArray): Integer;
var
  I, J: Integer;
begin
  Result := 0;
  for I := Low(Solutions) to High(Solutions) do
    with Solutions[I] do begin
      Panorama := Level;
      for J := Low(FPertinentPanoramas[I, Level])
          to High(FPertinentPanoramas[I, Level]) do
        if Level > Solutions[FPertinentPanoramas[I, Level, J]].Level then
          Panorama := Panorama + 1
        else
          Panorama := Panorama - 1;
      Result := Result + Panorama;
    end;
end;

相关类型定义:

TSolution = record
  Level, Panorama: Integer;
end;
TSolutionDynArray = array of TSolution;

其中FPertinentPanoramas是初始化后只读的整数多维数组。理论上,循环中每个I的计算逻辑完全独立,可通过拆分I的区间实现并行化,但自行实现的复用线程版本(代码如下)性能反而远低于单线程:

type
  TAnonymousThread = class(TThread)
  private
    FEvent: TEvent;
    FProc: TProc;
  protected
    procedure Execute; override;
    procedure SetProc(const Proc: TProc);
  public
    constructor Create(const Proc: TProc = nil);
    property Proc: TProc write SetProc;
  end;

var
  AnonymousThreads: array of TAnonymousThread;
  Event: TEvent;

procedure InitializeThreads(const N: Integer);
procedure ExecuteAnonymousThreads;

implementation

var
  FCriticalSection: TCriticalSection;
  FCounter: Integer;

procedure Counter;
begin
  with FCriticalSection do begin
    Acquire;
    FCounter := FCounter - 1;
    if FCounter = 0 then
      Event.SetEvent;
    Release;
  end;
end;

constructor TAnonymousThread.Create(const Proc: TProc = nil);
begin
  inherited Create;
  FreeOnTerminate := True;
  FEvent := TEvent.Create;
  SetProc(Proc);
end;

procedure TAnonymousThread.Execute;
begin
  while not Terminated do begin
    FEvent.WaitFor;
    FEvent.ResetEvent;
    if Assigned(FProc) then
      FProc;
    Counter;
  end;
end;

procedure TAnonymousThread.SetProc(const Proc: TProc);
begin
  FProc := Proc;
end;

procedure InitializeThreads(const N: Integer);
var
  I: Integer;
begin
  Event := TEvent.Create;
  FCriticalSection := TCriticalSection.Create;
  SetLength(AnonymousThreads, N);
  for I := Low(AnonymousThreads) to High(AnonymousThreads) do
    AnonymousThreads[I] := TAnonymousThread.Create;
end;

procedure ExecuteAnonymousThreads;
var
  I: Integer;
begin
  FCounter := Length(AnonymousThreads);
  Event.ResetEvent;
  for I := Low(AnonymousThreads) to High(AnonymousThreads) do
    AnonymousThreads[I].FEvent.SetEvent;
end;

补充测试情况:

  • 尝试硬编码非匿名线程,性能无明显提升;
  • 在Panorama函数内部使用TParallel.For性能极差,但在函数外层调用(每个任务处理整个SolutionsPool[I])效果良好;
  • OTL的Parallel.ForEach比TParallel.For性能高约10%;
  • 自行实现的TParallelFor类性能比TParallel.For高100%以上,但依赖任务拆分的均匀性(理想情况为(HighA - LowA + 1) mod N = 0),且任务计算量越大,越适合高并行度。

性能劣于单线程的原因

  1. 同步与调度开销过高
    自行实现的线程模型中,每个线程依赖Event等待任务触发,且每次任务完成后都要进入临界区更新计数器,这些同步操作的开销远高于单个I循环的计算开销,直接抵消了并行带来的收益。

  2. 任务粒度不合理
    如果拆分的任务粒度太小(比如每个I单独作为一个任务),线程切换、任务调度的开销会远超过并行计算节省的时间,尤其是当每个I对应的J循环计算量不大时,这种情况会更加明显。

  3. 线程池实现低效
    该匿名线程池的设计存在冗余:线程长期处于等待Event的状态,每次任务需要手动设置Proc并触发Event,额外增加了调度复杂度;若线程数量超过CPU核心数,会导致频繁的上下文切换,进一步降低性能。

  4. 缓存一致性问题
    多个线程同时访问共享的Solutions数组时,若不同线程修改的元素位于同一缓存行,会触发伪共享,导致缓存频繁失效并同步,增加内存访问开销。


优化方案

  1. 调整任务粒度
    避免细粒度任务,将I的循环区间按CPU核心数拆分为大块连续区间(比如4核拆为4段),每个线程处理一整段区间,减少任务调度和同步的次数。同时,优先让线程处理连续的内存区域,提升缓存命中率。

  2. 优化结果汇总方式
    原函数中Result是全局累加,并行时需要同步。改为让每个线程先计算局部结果,最后再将所有线程的局部结果汇总到全局,彻底避免频繁的临界区操作。

  3. 使用成熟并行库
    优先选择OTL的Parallel.ForEach或自行实现的TParallelFor类(已验证性能更优),这类库已经优化了线程调度、任务拆分和同步机制,比自行实现的简单线程池效率高得多。

  4. 优化内存访问

    • 将TSolution结构体设置为缓存行对齐(比如使用[Align(64)]属性),避免伪共享;
    • 由于FPertinentPanoramas是只读的,可在并行计算前将其预加载到CPU缓存,减少内存访问延迟。
  5. 合理设置并行度
    根据任务计算量调整线程数:计算量小的任务减少并行度(比如等于CPU核心数),计算量大的任务可适当增加,但不宜超过核心数的2倍,避免过多上下文切换。

  6. 避免不必要的线程复用
    如果任务不是频繁执行,复用线程的收益可能不如直接使用轻量级任务(如TTask),因为复用线程的等待、触发机制存在额外开销,而轻量级任务由系统线程池调度,效率更高。


内容的提问来源于stack exchange,提问作者hundreAd

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.30 15:19:32