【问题标题】:Comparing memory buffers比较内存缓冲区
【发布时间】:2011-08-25 11:57:01
【问题描述】:

我正在使用 Delphi 和一个内部 ISAM 数据库。

我有一个函数可以将表中的记录返回到指针类型的缓冲区中,一次一条记录。我正在尝试在从表中读取记录时将它们组合在一起。

目前是这样的:

从磁盘读取记录 如果记录不在记录组列表中,则将其添加到列表中 否则丢弃。

该函数基本上将记录与列表中当前的所有其他记录进行比较,直到找到匹配项或到达列表末尾。

我使用 CompareMem 将记录与列表中的记录进行比较,但这很慢。我发现了一个哈希函数,它可以使用指针而不是字符串,而且似乎工作正常。

一条记录由许多不同数据类型(字符串、整数、浮点数、布尔值等)的字段组成。

使用散列函数更快一些,并且需要更少的内存,因为我现在只需要存储散列值而不是记录的副本。

我确信有更好的方法来做到这一点。有人可以给我一些关于如何正确执行此操作的建议吗?我认为散列是要走的路,我使用的散列函数还可以,但速度不是很快,也许有人可以推荐一个散列函数?。

数据是会计记录、客户信息、股票记录,基本上是您在典型会计应用程序数据库中可以找到的任何内容。

谢谢

【问题讨论】:

  • 您可能可以通过使用更大的缓冲区来优化从磁盘读取,但最终您应该使用分析器来缩小您花费时间的确切范围。
  • 至于散列函数,我的第一个选择是尝试 SHA-1 或 MD5 散列。您可以尝试变得聪明并计算两个哈希值。一个是数据的子集,另一个是整个记录。对于您的大多数搜索,计算“快速”哈希可能足以确定记录不相等,因此您可以继续下一个。
  • @Lieven 加密级别的 SHA-1 或 MD5 不适合用于多个值的此类比较。他们的目的是检查数据的完整性。看我的回答。
  • @Arnoud - 我从中学到了很多东西。看我的评论:)
  • 您是否考虑过调整您的 SQL 使其只返回不同的结果?然后您不必自己删除重复项。

标签: database delphi hash


【解决方案1】:

1.使用哈希函数搜索

以下是在搜索中实现散列函数的方法:

  • 每条记录都有自己的可用哈希值 - 应存储在数据库中以节省时间:您不必每次都计算它;
  • 要查找记录是否已存在,计算其哈希值,然后将此哈希与哈希列表进行比较 - 匹配时,使用 CompareMem 确定(可能会发生冲突,因为没有完美的哈希函数)。

您有两种方法可以将新记录哈希与哈希列表进行比较:

  • 蛮力只是循环遍历所有哈希,并比较它们 - 比 CompareMem 快,但会是 O(n),即添加记录时会变慢;
  • 使用哈希查找表而不是哈希列表 - 此查找表大小通常是 2 的幂,大于主哈希列表。

哈希查找表是最好的选择,恕我直言。这就是 Generics.Collections 单元在现代 Delphi 中实现这一点的方式。如果可以,请使用它。

对于使用纯 record 内容(包括字符串和嵌套动态数组)的变体,您可以查看我们的 TDynArrayHashed 包装器。

在这种情况下,查找表只是一个包含 32 位无符号哈希值和数组中元素索引的数组:

  TSynHash = record
    Hash: cardinal;
    Index: cardinal;
  end;

  TSynHashDynArray = array of TSynHash;

例如,哈希表查找是如何从头开始填充的:

procedure TDynArrayHashed.ReHash;
var i, n, PO2, ndx: integer;
    P: PAnsiChar;
    aHashCode: cardinal;
begin
  // find nearest power of two for new fHashs[] size
  SetLength(fHashs,0); // any previous hash is invalid
  n := Capacity+1; // use Capacity instead of Count for faster process 
  PO2 := 256;
  while PO2<n do
    PO2 := PO2 shl 1;
  SetLength(fHashs,PO2);
  // hash all dynamic array values
  P := Value^;
  for i := 0 to Count-1 do begin
    aHashCode := HashOne(P^);
    ndx := HashFind(aHashCode,P^);
    if ndx<0 then
      // >=0 -> already found -> not necessary to add duplicated hash
      with fHashs[-ndx-1] do begin
        Hash := aHashCode;
        Index := i;
      end;
    inc(P,ElemSize);
  end;
end;

这是在哈希查找表中搜索项目的方式:

function TDynArrayHashed.HashFind(aHashCode: cardinal; const Elem): integer;
var n, first: integer;
    looped: boolean;
begin
  looped := false;
  n := length(fHashs);
  result := (aHashCode-1) and (n-1); // fHashs[] has a power of 2 length
  first := result;
  repeat
    with fHashs[result] do
    if Hash=aHashCode then begin
      if ElemEquals(PAnsiChar(Value^)[Index*ElemSize],Elem) then begin
        result := Index;
        exit; // found -> returns index in dynamic array
      end;
    end else
    if Hash=0 then begin
      result := -(result+1);
      exit; // not found -> returns void index in fHashs[] as negative
    end;
    // hash colision -> search next item
    inc(result);
    if result=n then
      // reached the end -> search once from fHash[0] to fHash[first-1]
      if looped then
        Break else begin
        result := 0;
        n := first;
        looped := true;
      end;
  until false;
  raise Exception.Create('HashFind'); // we should never reach here
  result := -1; // mark not found
end;

它调用ElemEquals 来深入比较记录内容,以防哈希冲突(可能总是发生)。此函数使用 Delphi RTTI 处理记录内容中的嵌套字符串和(动态)数组。如果你的记录只是一个二进制缓冲区,你可以在这里使用CompareMem

2。哈希函数

关于要使用的散列函数,有几个。如果您需要哈希查找表,则不需要加密级别的哈希函数(如 SHA-1、MD5 或 SHA-256),因为您将根据哈希查找表的大小进行取模,而这些函数很多比下面的慢。

一个简单的返回红衣主教就可以解决问题 - 这方面的唯一问题是避免大多数碰撞。更快的是 Adler32(或我们的 Hash32),但您可以使用经典但仍然很快的 Kernighan & Ritchie 哈希函数,或 crc32(例如,如果您使用 zip,它应该在您的代码中可用)。

下面是一些返回基值的代码,来自我们的开源库。每个版本都有一个优化的汇编函数和一个“纯帕斯卡”版本,非常适合 ARM 或 64 位。

Kernighan & Ritchie 哈希函数

function kr32(crc: cardinal; buf: PAnsiChar; len: cardinal): cardinal;
{$ifdef PUREPASCAL}
var i: integer;
begin
  for i := 0 to len-1 do
    crc := ord(buf[i])+crc*31;
  result := crc;
end;
{$else}
asm // eax=crc, edx=buf, ecx=len
    or ecx,ecx
    push edi
    push esi
    push ebx
    push ebp
    jz @z
    cmp ecx,4
    jb @s
@8: mov ebx,[edx] // unrolled version reading per DWORD
    lea edx,edx+4
    mov esi,eax
    movzx edi,bl
    movzx ebp,bh
    shr ebx,16
    shl eax,5
    sub eax,esi
    lea eax,eax+edi
    mov esi,eax
    shl eax,5
    sub eax,esi
    lea esi,eax+ebp
    lea eax,eax+ebp
    movzx edi,bl
    movzx ebx,bh
    shl eax,5
    sub eax,esi
    lea ebp,eax+edi
    lea eax,eax+edi
    shl eax,5
    sub eax,ebp
    cmp ecx,8
    lea eax,eax+ebx
    lea ecx,ecx-4
    jae @8
    or ecx,ecx
    jz @z
@s: mov esi,eax
@1: shl eax,5
    movzx ebx,byte ptr [edx]
    lea edx,edx+1
    sub eax,esi
    dec ecx
    lea esi,eax+ebx
    lea eax,eax+ebx
    jnz @1
@z: pop ebp
    pop ebx
    pop esi
    pop edi
end;
{$endif}

Hash32(修改后的adler32函数)

function Hash32(Data: pointer; Len: integer): cardinal;
{$ifdef PUREPASCAL} // this code is quite as fast as the optimized asm below
function SubHash(P: PCardinalArray; L: integer): cardinal;
{$ifdef HASINLINE}inline;{$endif}
var s1,s2: cardinal;
    i: PtrInt;
const Mask: array[0..3] of cardinal = (0,$ff,$ffff,$ffffff);
begin
  if P<>nil then begin
    s1 := 0;
    s2 := 0;
    for i := 1 to L shr 4 do begin // 16 bytes (4 DWORD) by loop - aligned read
      inc(s1,P^[0]);
      inc(s2,s1);
      inc(s1,P^[1]);
      inc(s2,s1);
      inc(s1,P^[2]);
      inc(s2,s1);
      inc(s1,P^[3]);
      inc(s2,s1);
      inc(PtrUInt(P),16);
    end;
    for i := 1 to (L shr 2)and 3 do begin // 4 bytes (DWORD) by loop
      inc(s1,P^[0]);
      inc(s2,s1);
      inc(PtrUInt(P),4);
    end;
    inc(s1,P^[0] and Mask[L and 3]);      // remaining 0..3 bytes
    inc(s2,s1);
    result := s1 xor (s2 shl 16);
  end else
    result := 0;
end;
begin // use a sub function for better code generation under Delphi
  result := SubHash(Data,Len);
end;
{$else}
asm // our simple and efficient algorithm (ADLER-32 based) is:
    //   while(data) do { s1 := s1+DWORD(data); s2 := s2+s1; }
    //   return (s1 xor (s2 shl 16));
    // this asm code is very optimized for modern pipelined CPU
    or eax,eax
    push ebx
    jz @z
    mov ecx,edx     // ecx = length(Data)
    mov edx,eax     // edx = Data
    xor eax,eax     // eax = s1 = 0
    xor ebx,ebx     // ebx = s2 = 0
    push ecx
    shr ecx,2
    jz @n
    push ecx
    shr ecx,2
    jz @m
    nop; nop
@16:add eax,[edx]   // 16 bytes (4 DWORD) by loop - aligned read
    add ebx,eax
    add eax,[edx+4] // both 'add' are pipelined: every DWORD is processed at once
    add ebx,eax
    add eax,[edx+8]
    add ebx,eax
    add eax,[edx+12]
    add ebx,eax
    dec ecx
    lea edx,edx+16
    jnz @16
@m: pop ecx
    and ecx,3
    jz @n
    nop
@4: add eax,[edx]  // 4 bytes (DWORD) by loop
    add ebx,eax
    dec ecx
    lea edx,edx+4
    jnz @4
@n: pop ecx
    mov edx,[edx] // read last DWORD value
    and ecx,3     // remaining 0..3 bytes
    and edx,dword ptr [@Mask+ecx*4] // trim to DWORD value to 0..3 bytes
    add eax,edx
    add ebx,eax
    shl ebx,16
    xor eax,ebx  // return (s1 xor (s2 shl 16))
@z: pop ebx
    ret
    nop; nop // align @Mask
@Mask: dd 0,$ff,$ffff,$ffffff // to get only relevant byte information
end;
{$endif}

crc32 函数

{$define BYFOUR}
// if defined, the crc32 hashing is performed using 8 tables, for better
// CPU pipelining and faster execution

var
  // tables content is created from code in initialization section below
  // (save 8 KB of code size from standard crc32.obj, with no speed penalty)
  crc32tab: array[0..{$ifdef BYFOUR}7{$else}0{$endif},byte] of cardinal;

function crc32(crc: cardinal; buf: PAnsiChar; len: cardinal): cardinal;
// adapted from fast Aleksandr Sharahov version
asm
{$ifdef BYFOUR}
  test edx, edx
  jz   @ret
  neg  ecx
  jz   @ret
  not eax
  push ebx
@head:
  test dl, 3
  jz   @bodyinit
  movzx ebx, byte [edx]
  inc  edx
  xor  bl, al
  shr  eax, 8
  xor  eax,dword ptr [ebx*4 + crc32tab]
  inc  ecx
  jnz  @head
  pop  ebx
  not eax
@ret:
  ret
@bodyinit:
  sub  edx, ecx
  add  ecx, 8
  jg   @bodydone
  push esi
  push edi
  mov  edi, edx
  mov  edx, eax
@bodyloop:
  mov ebx, [edi + ecx - 4]
  xor edx, [edi + ecx - 8]
  movzx esi, bl
  mov eax,dword ptr [esi*4 + crc32tab + 1024*3]
  movzx esi, bh
  xor eax,dword ptr [esi*4 + crc32tab + 1024*2]
  shr ebx, 16
  movzx esi, bl
  xor eax,dword ptr [esi*4 + crc32tab + 1024*1]
  movzx esi, bh
  xor eax,dword ptr [esi*4 + crc32tab + 1024*0]
  movzx esi, dl
  xor eax,dword ptr [esi*4 + crc32tab + 1024*7]
  movzx esi, dh
  xor eax,dword ptr [esi*4 + crc32tab + 1024*6]
  shr edx, 16
  movzx esi, dl
  xor eax,dword ptr [esi*4 + crc32tab + 1024*5]
  movzx esi, dh
  xor eax,dword ptr [esi*4 + crc32tab + 1024*4]
  add ecx, 8
  jg  @done
  mov ebx, [edi + ecx - 4]
  xor eax, [edi + ecx - 8]
  movzx esi, bl
  mov edx,dword ptr [esi*4 + crc32tab + 1024*3]
  movzx esi, bh
  xor edx,dword ptr [esi*4 + crc32tab + 1024*2]
  shr ebx, 16
  movzx esi, bl
  xor edx,dword ptr [esi*4 + crc32tab + 1024*1]
  movzx esi, bh
  xor edx,dword ptr [esi*4 + crc32tab + 1024*0]
  movzx esi, al
  xor edx,dword ptr [esi*4 + crc32tab + 1024*7]
  movzx esi, ah
  xor edx,dword ptr [esi*4 + crc32tab + 1024*6]
  shr eax, 16
  movzx esi, al
  xor edx,dword ptr [esi*4 + crc32tab + 1024*5]
  movzx esi, ah
  xor edx,dword ptr [esi*4 + crc32tab + 1024*4]
  add ecx, 8
  jle @bodyloop
  mov eax, edx
@done:
  mov edx, edi
  pop edi
  pop esi
@bodydone:
  sub ecx, 8
  jl @tail
  pop ebx
  not eax
  ret
@tail:
  movzx ebx, byte [edx + ecx]
  xor bl,al
  shr eax,8
  xor eax,dword ptr [ebx*4 + crc32tab]
  inc ecx
  jnz @tail
  pop ebx
  not eax
{$else}
  test edx, edx
  jz @ret
  neg ecx
  jz @ret
  not eax
  sub edx,ecx
  push ebx
@next:
  movzx ebx, byte [edx + ecx]
  xor bl, al
  shr eax, 8
  xor eax, [ebx*4 + crc32tab]
  add ecx, 1
  jnz @next
  pop ebx
  not eax
@ret:
{$endif BYFOUR}
end;

and the associated code to create the tables


procedure InitCrc32Tab;
var i,n: integer;
    crc: cardinal;
begin // this code size is only 105 bytes, generating 8 KB table content  
  for i := 0 to 255 do begin
    crc := i;
    for n := 1 to 8 do
      if (crc and 1)<>0 then
        // $edb88320 from polynomial p=(0,1,2,4,5,7,8,10,11,12,16,22,23,26)
        crc := (crc shr 1) xor $edb88320 else
        crc := crc shr 1;
    crc32tab[0,i] := crc;
  end;
{$ifdef BYFOUR}
  for i := 0 to 255 do begin
    crc := crc32tab[0,i];
    for n := 1 to 7 do begin
      crc := (crc shr 8) xor crc32tab[0,byte(crc)];
      crc32tab[n,i] := crc;
    end;
  end;
{$endif}
end;

使用 BYFOUR 定义,这个 crc32 速度非常快,并且与 Hash32 或 KR32 相比,冲突更少。

有关哈希函数的比较,请参阅this great article

源码见our source code repository

【讨论】:

  • +1 我假设已经使用了哈希查找表。重读,我认为你是对的 OP 正在使用“蛮力”搜索。关于非加密哈希的速度的很好的指针,我真的不知道,谢谢。
  • 感谢您的回答。我正在使用 Delphi 2006 我需要找到自己的散列函数。 Hash32 看起来还不错。
  • 我已经实现了一个带有碰撞检测的哈希表。现在快多了。谢谢大家的cmets。
【解决方案2】:

“更好的方法”是从一开始就避免这个问题。修改您的 SQL,使其只返回不同的记录,然后您不必担心后处理来删除重复项。

【讨论】:

    【解决方案3】:

    请注意(除非您使用的是 ShortString)CompareMem 不会找到重复的记录,因为字符串的字符不是直接存储在记录/类中,而是指针。简单地根据字节计算散列的散列函数也会有同样的问题。

    因此,要进行哈希,您将需要使用所有简单值类型(整数、浮点数、布尔值等)的字节,并且对采用字符串指向的字节的字符串有特殊情况。为了更快地计算,您可以通过使用前几个字符与最后一个字符组合来简化它,因为无论如何您都必须注意哈希冲突。

    如果您使用的是 Delphi 2009 或更高版本,Generics.Defaults-unit 中有一个散列函数。您也可以尝试使用该算法的 Generics.Collections 中的 Dictionary-class,

    【讨论】:

    • 根据 OP 的解释,我假设从数据库中检索的记录是一个连续的内存块。在这种情况下,哈希解决方案将起作用。
    • @Lieven:再次阅读这个问题,我认为您可能是对的。我对泛型单元的建议是。
    • 关于嵌套字符串或动态数组的记录(例如),你是对的。但是您可以使用 RTTI 创建哈希或将记录的内容与嵌套字段内容进行比较。这就是我们的TDynArray 所做的。
    【解决方案4】:

    我的建议是 MD5——它非常快。有一些 MD5 实现,特别是在 OpenSSL 中,用汇编语言编写,速度惊人。 32 位或 64 位 Intel 处理器的实现具有大约或略低于每字节 5 个 CPU 周期的性能(在具有 Skylake 微架构的处理器上)。在为 CRC32 发明 Slicing-by-8 之前,它的性能与 CRC-32 大致相同。然后 CRC32 实现为 Slicing-by-8,在上述处理器上每个字节大约 1.20 个 CPU 周期。

    尽管由于冲突,MD5 不再被视为强加密哈希函数,但在实践中,您的应用程序不会遇到任何冲突。 MD5 的好处是它的摘要大小与其他哈希函数相比非常小,因此不会浪费空间。

    如果您的现代 CPU 具有 SHA-1 的硬件实现,请改用 SHA-1。有以下指令:SHA1RNDS4、SHA1NEXTE、SHA1MSG1、SHA1MSG2,在Intel Goldmont微架构和AMD Ryzen上引入。

    或者,如果您的处理器支持 ASE-NI,您可以使用 AES 计算摘要。只需使用 AES-CBC 加密您的消息。不要忘记在加密之前根据 CMS 规则添加适当的填充。使用常量密钥和常量 IV(初始化向量),但只需确保它们包含真正的随机位(此要求对密钥特别强,而不是 IV)。所以最后一个加密块(128 位)将是您的摘要。如果您的处理器支持 AES-NI,则 AES 非常快。

    或者,如果您的处理器可以加速 CRC-32(CRC32 指令)或者您使用 Slicing-By-8 CRC 实现,您也可以使用 CRC-32。但 CRC-32 可能只有在您有少量消息或消息且消息大小较小的情况下才适用。我不能告诉你确切的数字——这取决于你愿意承担的风险。

    【讨论】:

    • MD5 不是很快。实际上,它最初的设计目的是为了防止快速计算校验和以查找冲突。 CRC32 更快,即使处理器不直接支持它。
    • @dummzeuch 我已经指定 MD5 大约是每个字节 5 个 CPU 周期,而 Slicing-8 之前的 CRC32 大约相同,而 Slicing-8 是每个字节 1,2 个 CPU 周期。你看过这些数字吗?
    猜你喜欢
    • 2020-02-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多