【问题标题】:How do I efficiently handle multiple inserts into an array?如何有效地处理对数组的多次插入?
【发布时间】:2013-09-30 15:05:28
【问题描述】:

我有一个动态分配的整数数组,我想在任意位置插入整数。许多整数如超过 250 万。

我的代码目前如下所示:

type
  TIntegerArr = array of Integer;

var
  FCount: Integer;
  FSortedList: TIntegerArr;

procedure Insert(_Value: Integer; _InsertPos: integer);
var
  OldList: TIntegerArr;
begin
  OldList := FSortedList;
  if Length(FSortedList) < FCount + 1 then begin
    OldList := FSortedList;
    FSortedList := nil;
    SetLength(FSortedList, FCount + 100);
    CopyMemory(@FSortedList[0], @OldList[0], SizeOf(Integer) * _InsertPos);
  end;
  MoveMemory(@FSortedList[_InsertPos + 1], @OldList[_InsertPos], SizeOf(Integer) * (FCount - _InsertPos));
  FSortedList[_InsertPos] := _Value;
  Inc(FCount);
end;

(真正的代码是一个类的方法,字段为FSortedList和FCount。)

使用临时列表并使用 Move 而不是 for 循环来移动数据已经大大提高了性能,因为它可以防止数组在必须增长时被复制两次(一次在现有数组上的 SetLength 和另一个移动时间)。

但最坏的情况 Insert(SomeValue, 0) 仍然总是移动所有现有值。

到目前为止,我一直在考虑在数组的开头引入偏移量,因此不必每次在前面插入新值时都必须移动所有现有值,我只能在偏移量达到时才这样做0. 例如:

// simple case: inserting at Position 0:
if FOffset = 0 then begin
  // [...] reallocate a new array as above
  Move(@FSortedList[100], @OldList, SizeOf(Integer) * _InsertPos);
  FOffset := 100;
end;
Dec(FOffset);
FSortedList[FOffset] := _NewValue;

(此代码未经测试,可能有问题) 这当然可以扩展以检查插入点是否更接近开头或结尾,并取决于将第一个或最后一个值移动一个位置,以便平均只有 1/4 的条目必须移动而不是现在的 1/2。

另一种选择是实现稀疏数组。我记得在 1990 年代的某个商业库中看到过这样的实现,但不记得是哪个(TurboPower?)。

这个过程是一些排序和索引代码的核心,这些代码适用于不同大小的数组,从几十个条目到上面提到的数百万个条目。

目前程序运行大约 2 小时(在我的优化之前它接近 5 小时),我已经知道数组中的条目数将至少增加一倍。随着插入性能越差,数组已经越大,我怀疑条目数量增加一倍时,运行时间至少会增加四倍。

我想要一些关于如何调整性能的建议。内存消耗目前不是什么大问题,但运行时间肯定是。

(这是 Delphi 2007,但除非较新的 Delphi 版本已经具有用于执行上述操作的优化库。Classes.TList 未优化。)

Edit1:刚刚找到我上面提到的稀疏数组实现:它是 TurboPower SysTools 的 StColl。

Edit2:好的,一些背景知识:我的程序读取一个包含当前 240 万个条目的 DBase 表,并从这些条目中生成几个新表。新表被规范化并在创建后被索引(出于性能原因,我在插入数据之前不生成索引,相信我,我先尝试过。)。数组是为生成的表提供内部排序的核心代码。新记录只追加到表中,但它们的 RecNo 按排序顺序插入到数组中。

【问题讨论】:

  • 请参阅 Improved Sliced Array implementation@Runner,如果这提供了如何更好地进行排序的任何输入。
  • @LURD:谢谢。我在他写这篇博文时读过(该页面上的第一条评论是我的)但已经忘记了。
  • 告诉我们更多关于这个“可插入数组”的用例。可能的解决方案取决于它们。
  • 在你的 Edit2 之后,我仍然不确定你是否真的需要一个数组,或者另一个容器会是更好的选择......
  • 你可以试试我的NLDSparseList

标签: arrays delphi delphi-2007 dynamic-arrays


【解决方案1】:

查看您的程序后,我立即发现了一些缺陷。为了查看进度,我首先在最坏的情况下测量了您现有程序的速度(始终在 0 位置添加数字)。

n:=500000;
for i:=0 to n-1
 do Insert(i, 0);

测量:n=500000 47.6 毫秒

A) 简单

我从你的程序中删除了一些不必要的行(OldList 完全没有必要,SetLength 保留了内存)。

改进 A:

procedure Insert(_Value: Integer; _InsertPos: integer);
begin
 if Length(FSortedList) < FCount + 1
    then SetLength(FSortedList, FCount + 100);
  Move(FSortedList[_InsertPos], FSortedList[_InsertPos+1], SizeOf(Integer) * (FCount - _InsertPos));
  FSortedList[_InsertPos] := _Value;
  Inc(FCount);
end;

速度增益6% (44.8 ms)

B) 一切都很重要

if Length(FSortedList) < FCount + 1 
   then SetLength(FSortedList, FCount + 100);
  • 提示 1:每次插入时都会调用函数 Length
  • 提示2:FCount+1 每次计算
  • 提示 3:过程参数为 const(通过引用)
  • 提示 4:引入 FCapacity 变量
  • 提示 5:仅将长度增加 100 会导致大量重新分配(250 万个数组上的 25.000)。正如您所说,内存不是问题,那么为什么不预先分配全部或至少大?

改进 B:

procedure Insert(const _Value, _InsertPos: integer);
begin
 if FCount = FCapacity
    then begin
     Inc(FCapacity, 100000);
     SetLength(FSortedList, FCapacity);
    end;
 Move(FSortedList[_InsertPos], FSortedList[_InsertPos+1], SizeOf(Integer) * (FCount - _InsertPos));
 FSortedList[_InsertPos] := _Value;
 Inc(FCount);
end;

速度增益1% (44.3 ms)。

提示:你可以实现一些渐进式算法,而不是 Inc by 100000。

C) 瓶颈

如果我们现在看这个过程,什么都没有,只是移动了很多内存。如果我们不能改变算法,我们必须改进内存移动。

实际上有 fastmove 挑战 (fastcode.sourceforge.net)

我准备了一个 zip,其中只有您需要的那些文件(3 个文件,源代码)。链接>>>http://www.dakte.org/_stackoverflow/files/delphi-fastcode.zip

  • 将 fastcodeCPUID.pas 和 fastmove.pas 添加到您的项目中!
  • 插入使用 fastmove.pas;
  • 瞧!没有什么可改变的!

我的机器上的速度提高了近 50%(取决于您使用的 CPU)。

原程序

n         ms graph
---------------------------------
100000   1.8 *
200000   7.6 ***
300000  17.0 *******
400000  30.1 *************
500000  47.6 ********************

改进,没有快速移动 (-7%)

n         ms graph
---------------------------------
100000   1.6 *
200000   6.9 ***
300000  15.7 ******
400000  28.2 ***********
500000  44.3 ******************

改进,使用 fastmove (-46%)

n         ms graph
---------------------------------
100000   0.8 *
200000   3.8 **
300000   9.0 ****
400000  16.3 *******
500000  25.7 ***********

最近的比赛:

 if FCount = FCapacity
    then begin
     if FCapacity<100000
        then FCapacity:=100000  
        else FCapacity:=FCapacity*2;
     SetLength(FSortedList, FCapacity);
    end;

正如我所说,您可以添加一些渐进式 FCapacity 增加。这是一些经典的 Grow 实现(如果需要,只需添加更多 if's 或将 100000 更改为更合适的值)。

D) 更新 2:数组为 ^TArray

type
  PIntegerArr3 = ^TIntegerArr3y;
  TIntegerArr3y = array[0..1] of Integer;

var
 FCapacity3,
 FCount3: Integer;
 FSortedList3: PIntegerArr3;

procedure ResizeArr3(var aCurrentArr: PIntegerArr3; const aNewCapacity: Integer);
var lNewArr: PIntegerArr3;

begin
 GetMem(lNewArr, aNewCapacity*SizeOf(Integer));

 if FCount3>0 // copy data too
  then begin
    if aNewCapacity<FCount3
       then FCount3:=aNewCapacity; // shrink
    Move(aCurrentArr^[0], lNewArr^[0], FCount3*SizeOf(Integer));
  end;

 FreeMem(aCurrentArr, FCapacity3*SizeOf(Integer));
 FCapacity3:=aNewCapacity;
 aCurrentArr:=lNewArr;
end;

procedure FreeArr3;
begin
 if FCapacity3>0
  then begin
    FreeMem(FSortedList3, FCapacity3*SizeOf(Integer));
    FSortedList3:=nil;
  end;
end;

procedure Insert3(const _Value, _InsertPos: integer);
begin
 if FCount3 = FCapacity3
    then ResizeArr3(FSortedList3, FCapacity3 + 100000);
 Move(FSortedList3^[_InsertPos], FSortedList3^[_InsertPos+1], SizeOf(Integer) * (FCount3 - _InsertPos));
 FSortedList3^[_InsertPos] := _Value;
 Inc(FCount3);
end;

步骤 C) 的速度增益无!

结论:FastMove或算法改变,内存移动速度达到“物理”极限!

我正在使用 Delphi XE3 和 System.pas,第 5307 行:

(* ***** BEGIN LICENSE BLOCK *****
 *
 * The assembly function Move is licensed under the CodeGear license terms.
 *
 * The initial developer of the original code is Fastcode
 *
 * Portions created by the initial developer are Copyright (C) 2002-2004
 * the initial developer. All Rights Reserved.
 *
 * Contributor(s): John O'Harrow
 *
 * ***** END LICENSE BLOCK ***** *)

procedure Move(const Source; var Dest; Count: NativeInt);

所以实际上在 Delphi 中已经有了一些 Fastcode 例程,但包括那些直接从他们的网站(或我上面包含的链接)下载的例程,差异最大,几乎 50%(奇怪)。

【讨论】:

  • 感谢您的广泛回答。我不明白,“改进A”中的速度提升从何而来。我打算对 OldList 变量执行的操作是防止在 SetLength 调用中复制整个现有内容(正如您所说,它保留了现有内容)。所以我分配了一个新数组,只将旧数组的那部分从 0 复制到 InsertPos,然后将 InsertPos 之后的部分移动到新数组。这应该可以防止大约一半的数组内容被复制两次。
  • @dummzeuch 在涉及许多相同操作的情况下,500.000 甚至数百万次,每个函数调用都很重要。 A 的速度提高是因为: 1. 移除了局部变量,不需要堆栈;
  • @dummzeuch(字符限制)在您的原始程序中,您使用 SetLength SetLength(FSortedList, FCount + 100); 分配了新内存,SetLength 也将所有内存归零,因此释放内存、分配内存、清除内存和复制清除完全是多余的记忆。如果您只调用 SetLength,它会分配新内存并复制当前内容,但只会将数组的剩余部分归零(即使这种归零也是多余的),所以它只是一个两步过程,几乎没有冗余;)
  • 所以,如果我理解正确的话,除了由于没有堆栈分配、分配给一个变量(无论如何可能是一个 CPU 寄存器)以及对 CopyMemory 的调用在在位置 0 插入的特殊情况下,速度的提高基本上是通过防止 SetLength 不必要地将所有内存设置为 0。所以我使用整数数组而不是整数的 ^array[0...largenumber] 并分配自己记忆。对吗?
  • @dummzeuch 或多或少是的 ... ;) 使用程序中的指针,您可以更快地编写代码,但维护工作量更大,并且您失去了源代码的可读性,而且您必须释放任何已分配的内容,使用不需要的数组。但是,我在回答您的问题,并且您定义了一个整数数组。使用指针会更快,因为可以删除一些多余的东西;关于 CPU 寄存器的评论,Delphi 通过将最多三个参数直接放入寄存器来优化函数调用(如果可能:指针、32 位 cpu 上的 int8-int32 ......),从不使用局部变量。
【解决方案2】:

不要成为破坏者,但解决方案已经在我的问题的编辑中:

从阵列切换到TurboPower's StColl 后,大型阵列的性能不再下降,并且启动速度非常快。运行时间从 2 小时减少到不到 1/2 小时。改变真的很简单。我希望我早点记得那个图书馆。

我需要 SourceForge 存储库中的以下文件(我不想下载整个库):

  • StBase.pas
  • StColl.pas
  • StConst.pas
  • StList.pas
  • StDefine.inc

实际上,我很惊讶没有更多的相互依赖关系。 TurboPower 家伙肯定知道他们的行业。不知道他们今天在做什么,还在为赌场编程赌博机?

【讨论】:

  • 如果稀疏数组是答案,那么问题就错了。稀疏数组和数组是完全不同的。如果您有一个稀疏数组,那么您可以预先分配整个数组并且永远不要移动。在现代 Delphi 中,您会使用 TDictionary&lt;K,V&gt;
  • 好的,你是对的,我需要的是一个类似数组的整数数据结构,它允许我在任何随机位置有效地插入新条目。 TStCollection 提供了这个。我不需要 TStCollection 的可能性为此应用程序有未使用的间隙。
  • @DavidHeffernan 该评论(TDictionary 用作稀疏数组以提高性能)知识渊博!
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2017-12-18
  • 1970-01-01
  • 2016-12-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多