【问题标题】:Pointer(s)^ versus s[1]指针^与 s[1]
【发布时间】:2011-09-18 16:02:42
【问题描述】:

在从磁盘读取数据(数据意味着排他字符串)的函数中,我应该更喜欢哪个?哪个更好?

A) DiskStream.Read(Pointer(s)^, Count)
or
B) DiskStream.Read(s[1], Count)

注意:
我知道两者的结果相同。
我知道在调用 Read 之前我必须设置 S 的长度。


更新

S 是 AnsiString。

这是完整的功能:

{ 从文件中读取一堆字符。为什么是“ReadChars”而不是“ReadString”?此函数读取 C++ 字符串(字符串的长度也未写入磁盘)。所以,我必须给出要读取的字符数作为参数。 }

function TMyStream.ReadChars(out s: AnsiString; CONST Count: Longint): Boolean; 
begin
 SetLength(s, Count);
 Result:= Read(s[1], Count)= Count;
end;

速度测试

在我的速度测试中,第一种方法比第二种方法快一点。我使用了一个 400MB 的文件,从中读取了大约 200000 次字符串。该进程被设置为高优先级。

有史以来最好的阅读时间是:
变体 B 为 1.35,变体 A 为 1.37。
平均:
平均而言,B 的得分也比 A 高 20 毫秒。

对每个变体重复测试 15 次。

差别真的很小。它可能落入测量误差范围内。 如果我更频繁地从更大的文件中读取字符串,这可能会很重要。 但目前让我们假设两行代码的性能相同。

回答
变体 A - 可能会稍微快一点 变体 B - (显然)更容易阅读,并且更接近德尔福。我的首选。

注意:
我在 TStreamReadBuffer 示例中看到 Embarcadero 使用变体 A,但使用的是 TBytes 而不是 String。

【问题讨论】:

  • 这是哪个Delphi版本?由于 Unicode 更改,您可能会在 D2009+ 中使用这种方法遇到问题。 Nick Hodges 的这篇文章阐明了在较新的 Delphi 版本中应该如何做:edn.embarcadero.com/article/38693
  • 嗨,詹斯。我忘记了。我更新了我的问题以表明 s 是 AnsiString。
  • 刚刚看到您更新的速度测试。我想说 20 毫秒是值得为更好的代码可读性和避免因不调用 UniqueString 而可能出现的任何问题付出的代价,不是吗?
  • 当这些问题陷入对可疑优化的神秘讨论时,我讨厌它。为什么我们似乎对性能比对正确性更感兴趣?显然两者都很重要,但我真的强烈认为正确性必须放在首位。
  • @Altar:你说如果你从一个更大的文件中读取更多的字符串,差异会更大。这点考虑一下吧。您正在阅读一个 400 MB 的文件。将所有内容乘以 10,4 GB 文件的差异为 0.2 秒。您现在遇到了 32 位地址空间的限制,而您仍然只节省了微不足道的几分之一秒。真的不值得。

标签: delphi


【解决方案1】:

绝对是数组表示法。 Delphi 风格的一部分是使您的代码易于阅读,并且当您准确地说明您正在做什么时,更容易知道发生了什么。将字符串转换为指针然后取消引用它看起来很混乱;你为什么要这样做?除非读者对字符串内部有很多了解,否则这是没有意义的。

【讨论】:

  • 嗨梅森。我实际上正在使用第二个。我问是因为如果您进行 Google 搜索,您会发现 Delphi 程序员经常使用第一个。所以,我一直在寻找一个理由。
  • @Altar:原因主要是他们认为它更快。但是正如您在测试时看到的那样,性能提升可以忽略不计。谈到速度时,一个好的经验法则是“除非用户注意到差异,否则它不会变慢”。无论哪种方式,都没有人会注意到 20 毫秒,尤其是在不到 1.5 秒的总时间中。
  • 你是对的。起初我也认为他们是有原因的。现在,在执行测试后,我可以看到实数。 '接受'。
  • PS:变体 B 一直是我的首选,我一直在我的代码中使用它。我只是想看看人们为什么使用第一个变体。
【解决方案2】:

在运行时注意

1. DiskStream.Read(Pointer(s)^, Count)
2. DiskStream.Read(s[1], Count)

1.版本会更快。

但您必须确保s 变量是显式本地,或者您在循环之前已称自己为UniqueString(s)

由于pointer(s)^ 不会调用UniqueString?() 低级隐藏RTL 调用,它会比s[1] 更快,但如果s 字符串,您可以覆盖一些现有数据变量在当前上下文和其他上下文之间共享(例如,如果 s 的最后内容是从一个属性值的函数中检索到的,或者 s 作为参数发送到另一个方法)。

事实上,从内容中读取AnsiString 的最快正确编码方法是:

  s := '';
  SetLength(s,Count);
  DiskStream.Read(pointer(s)^,Count);

  SetString(s,nil,Count);
  DiskStream.Read(pointer(s)^,Count);

第二版与第一版相同,但少了一行。

s 设置为'' 将调用FreeMem()+AllocMem() 而不是SetLength() 中的ReallocMem(),因此将避免调用move(),因此会更快一些。

其实s[1]产生的UniqueString?() RTL调用会非常快,因为你在调用它之前已经调用了SetLength():因此,s已经是唯一的,UniqueString?() RTL调用会几乎立即返回。分析后,两个版本之间的速度差异不大:几乎所有时间都花在字符串分配和从磁盘移动内容上。也许s[1] 被发现更“pascalish”。

【讨论】:

  • +1 我不认为性能真的是这里的问题,但我非常感谢您准确处理有关UniqueString 的正确性问题。对我来说,这个答案的复杂性是选择s[1] 的完美理由。 ;-)
  • 我已将参数从 'var S: string' 更改为 'our s: string'。我认为这将解决 s:= '' 问题。
  • @Altar '我们的 s:string' 是什么意思? s := ''; 是为了避免在同一个变量上多次调用 SetLength() 时调用 realloc,例如在一个循环中。
  • @David 感谢您的意见。我们都同意pointer(s) 只有在您知道自己在做什么的情况下才应该使用。我想强调它可能是危险的!因此,如果 Delphi 编码人员对使用 pointer(s) 有最小的疑问,他/她应该避免它并依赖 s[1]! ;)
  • @Bouchez - 抱歉。我的意思是 OUT 而不是 OUR。像 in: 过程 xyz(out i: integer)
【解决方案3】:

如果您关心优化,您应该更喜欢第一个变体。看看编译器生成的代码就知道了:

Unit7.pas.98: Stream.Read(Pointer(S)^, 10);
00470EA9 8B55FC           mov edx,[ebp-$04]
00470EAC B90A000000       mov ecx,$0000000a
00470EB1 8BC6             mov eax,esi
00470EB3 8B18             mov ebx,[eax]
00470EB5 FF530C           call dword ptr [ebx+$0c]

Unit7.pas.99: Stream.Read(s[1], 10);
00470EB8 8B5DFC           mov ebx,[ebp-$04]
00470EBB 85DB             test ebx,ebx
00470EBD 7418             jz $00470ed7
00470EBF 8BC3             mov eax,ebx
00470EC1 83E80A           sub eax,$0a
00470EC4 66833802         cmp word ptr [eax],$02
00470EC8 740D             jz $00470ed7
00470ECA 8D45FC           lea eax,[ebp-$04]
00470ECD 8B55FC           mov edx,[ebp-$04]
00470ED0 E8CB3FF9FF       call @InternalUStrFromLStr
00470ED5 8BD8             mov ebx,eax
00470ED7 8D45FC           lea eax,[ebp-$04]
00470EDA E89950F9FF       call @UniqueStringU
00470EDF 8BD0             mov edx,eax
00470EE1 B90A000000       mov ecx,$0000000a
00470EE6 8BC6             mov eax,esi
00470EE8 8B18             mov ebx,[eax]
00470EEA FF530C           call dword ptr [ebx+$0c]

更新

以上代码由Delphi 2009 编译器生成。您可以使用 {$STRINGCHECKS OFF} 指令改进代码,但您仍然有UniqueStringU 函数调用开销:

Unit7.pas.100: Stream.Read(s[1], 10);
00470EB8 8D45FC           lea eax,[ebp-$04]
00470EBB E8B850F9FF       call @UniqueStringU
00470EC0 8BD0             mov edx,eax
00470EC2 B90A000000       mov ecx,$0000000a
00470EC7 8BC3             mov eax,ebx
00470EC9 8B18             mov ebx,[eax]
00470ECB FF530C           call dword ptr [ebx+$0c]

【讨论】:

  • 这是哪个 Delphi 版本,您是否启用了 StringChecks 选项(如果适用)?
  • 有趣。然而,这个答案忽略了问题的“什么是德尔福风格”部分。在 99.9% 的情况下,再多的一些操作并不重要。
  • @Altar:你知道现代CPU的时钟频率是多少数量级吗?
  • @Altar,性能差异可以忽略不计。始终追求可读性而不是性能,除非(量化的)性能增益证明降低的可读性是合理的。软件真正的“成本”是错误修复和维护——很少有性能是真正的问题,而且在几乎所有这些情况下,我们所说的不仅仅是百分之几。
  • @Mason - 如果DiskStream.Read(Pointer(s)^, Count) 是正确的,那么UniqueString 是开销。当然如果你写Pointer(s)^你应该明白在做什么,否则你会产生副作用。
【解决方案4】:

第二个选项肯定更“Delphi 风格”(如果您查看Delphi 版本的Windows API 标头,您会发现大多数指针参数都已转换为var 参数)。

除此之外,第二个选项不需要强制转换,恕我直言,可读性更强。

【讨论】:

  • 在 TStreamReadBuffer 的帮助页面中,我看到 Embarcadero 正在使用这个:Stream.ReadBuffer(Pointer(Buffer)^, Size) (虽然 Buffer 不是字符串而是 TBytes,但确实如此)。
  • IMO,Borland API 标头翻译应该用作模型,有很多 mispascalized 原型,需要客户端调用为fn(PDWORD(nil)^)
  • @Downvoter:我猜你的意思是“不用作模型”?在某些情况下可能翻译得不好,但肯定不会在大多数情况下。
  • @Smasher,大声笑,是的,不应该使用标题翻译,在构建可怕的引用-转换-去引用模式时真的很困惑 :) 不幸的是,有很多这样的。以及许多相反的情况(例如:最终nonpascalized WinSock)
  • @Downvoter 最近的 Delphi 版本已经处理了许多这些错误缩放的原型。你还在使用 Delphi 7 吗?
【解决方案5】:

我总是使用第二个保持类型安全的。我并不真正购买性能参数,因为您将在最坏的情况下访问磁盘、文件缓存或主内存,所有这些都会使少数 CPU 操作看起来有些微不足道。正确性应该比性能更重要。

但是,我要补充一点,这不应该让您太烦恼,因为您应该只编写一次此特定的代码。把它放在一个辅助类中并把它包好。随意关心优化,将其重写为汇编程序,无论您喜欢什么。但是d不会r重复y我们自己。

【讨论】:

  • “我真的不买性能参数,因为你要撞到磁盘了” - - - 你说的很对,除了 Windows 会为你缓存文件,所以在第二遍您将不再从磁盘读取,而是从 Windows 缓存 (RAM) 读取。速度优化可能是真实的。
  • @Altar 即使使用缓存,调用 ReadFile 的开销也将远远超过 A.Bouchez 所说的节省。现在,如果您是doing the buffering yourself,那么开销就会大大减少。 SetLength 的成本也可能更高。
  • @Altar:如果您想真正加快光盘访问速度,请不要通过这样的例程从光盘流式传输文件。相反,编写一个在 TFileStream 中打开文件的例程,然后将全部内容一次全部读取到 TMemoryStream 中,并使用 TMemoryStream 进行加载。性能提升非常惊人。 (当然,对于非常大的文件,这可能会导致问题,您需要尝试更复杂的技巧。但对于大多数东西来说,这是一个美妙而简单的速度提升。)
  • @David:我认为您的意思是 @Serg 作为引用答案的作者,而不是 A.Bouchez。
  • @Mason:+1,但是有两种情况应该提到这不是一个好方法:(a)如果您并不总是需要阅读整个文件和(b)如果文件很大
【解决方案6】:

如果有任何机会调用您的函数,计数为 0,那么 A) 将与 Pointer(s)^ 一起工作,只需评估为 nil 而 B) 将因范围检查异常而崩溃。

如果你想使用 B) 并且仍然优雅地处理 0 的计数,你应该使用:

function TMyStream.ReadChars(out s: AnsiString; const Count: Integer): Boolean; 
begin
 SetLength(s, Count);
 Result := (Count = 0)  or (Read(s[1], Count) = Count);
end;

【讨论】:

  • 感谢您提及。我读取的二进制文件不应该有空字符串。但正如你所说,我最好检查一下。
【解决方案7】:

第二个(DiskStream.Read(s[1], Count))。每当你遇到一个无类型的 var 参数时,它读起来就像“获取作为参数传递的地址”。因此,在这种情况下,您传递的是字符串 s 的第一个字符的地址,这正是您想要做的。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2014-06-26
    • 1970-01-01
    • 2016-10-30
    • 2020-12-30
    • 1970-01-01
    • 2011-08-04
    • 2019-05-10
    相关资源
    最近更新 更多