如何将PChar的一部分提取成字符串?

4

在分析过程中,我遇到了一个函数,它花费了相当多的时间,但本质上可以归结为这个非常简单的代码:

function GetSubstring(AInput: PChar; AStart, ASubstringLength: Integer): string;
begin
  Result := Copy(AInput, AStart, ASubstringLength);
end;

这个函数返回预期的子字符串,但在处理更长的输入时性能不佳。我查看了CPU视图中的汇编代码,并从中可以看出(通常我不是在汇编级别工作),似乎在调用Copy之前将AInput隐式转换为字符串。
但由于此时未知字符串/字符数组的长度,所以转换代码必须遍历PChar的长度,直到找到空终止符。这就解释了对于更长输入的可怕扩展性。
然而,由于调用者传递了PChar的长度,最初我认为可以将方法转换为使用SetString。
function GetSubstring(AInput: PChar; AStart, ASubstringLength: Integer): string;
begin
  SetString(Result, AInput + AStart - 1, ASubstringLength);
end;

除了 SetString 使用零索引(而不是像 Copy 一样使用基于一的索引)之外,似乎还有许多其他小事情要在验证输入方面做 Copy,其中并非所有都有记录(例如,任何小于1的起始值都会更改为1)。因此,上面的天真实现并不总是像原始实现那样工作。
我的目标是尽可能地复制 Copy 程序,因为该函数是库的一部分,并且已经被我的同事广泛使用。
我想知道以下实现是否完成了这一目标,或者是否需要注意 Copy 的任何其他注意事项。注意:FLength 是来自该功能所属模块中的另一个部分的 AInput 的实际长度。我为了这个例子而删除了那个其他部分。
function GetSubstring(AInput: PChar; AStart, ASubstringLength: Integer): string;
begin
  if (AInput = nil) then begin
    Result := '';
  end else begin
    if (AStart < 1) then begin
      AStart := 0;
    end else begin
      AStart := AStart - 1;
    end;
    if (ASubstringLength + AStart > FLength) then begin
      ASubstringLength := FLength - AStart;
    end;
    SetString(Result, AInput + AStart, ASubstringLength);
  end;
end;

我正在使用Delphi 2006,但我认为在产品的其他版本中(至少是非Unicode版本)并没有太大区别。


你可以尝试将 GetSubString() 进行内联。 - LU RD
1
除了SetString()是从零开始计数(而不是像Copy()一样从一开始计数)之外,它还没有索引。它是基于起始指针而不是索引操作的。Copy()有一个以1为基础的索引参数,因此原始的GetSubstring()也有一个以1为基础的AStart。你最初修改的GetSubstring()正在计算错误的指针。它需要使用-1而不是+1来保持与Copy()相同的语义(不包括角落情况):SetString(Result, Ainput + (AStart - 1), ASubstringLength); - Remy Lebeau
@RemyLebeau 您是对的。我过于简化了“Copy”和“SetString”之间的区别。此外,由于我无法访问我的工作机器,所以我不得不从记忆中重现代码,这导致了+1而不是-1和“SetString”函数/过程混淆。现在已经修复了这些问题。 - PersonalNexus
有趣的是,将方法内联化后,执行时间大约是非内联版本的两倍。不确定原因是否与我的测量方式有关;微基准测试存在很多问题。但是,不用完全扫描字符串并在每次复制时复制它已经足以提高性能了。 - PersonalNexus
2个回答

5
让我们考虑一些特殊情况,我认为它们如下:
  1. AInput 无效。
  2. AStart < 1
  3. AStart > FLength
  4. ASubstringLength < 0
  5. ASubstringLength + (AStart-1) > FLength
在我看来,我们可以忽略第一种情况。应该由调用者提供有效的 PChar。实际上,在我的观点中,你检查 AInput <> nil 已经是一个过于严格的步骤了,因为 nil 不是一个有效的 PChar
对于其余的情况,你已经涵盖了第2和第5种情况,但没有考虑第3和第4种情况。所以如果用户提供了一个过大的 AStart 值,那么你将读取字符串的末尾。同样,用户也可以轻松地提供负数的 ASubstringLength。我不认为你需要任何人编写代码来检查这些情况,因为你显然非常有能力。
现在,如果你真的关心每一点性能,你就不应该检查这些情况。要求用户传递有效的参数。在调试模式下,使用 {$IFOPF D+}Assert 来检查输入。当然,如果这些参数来自外部源,则应进行验证。
另一方面,原始代码遭受的最大性能损失是不必要地扫描整个字符串,并将其复制到中间堆分配的字符串中。一旦你已经删除了它们,正如你所做的那样,进一步提高性能的机会就大大降低了。

实际上,该代码确实使用了 System.SetString() 函数:procedure SetString(var s: string; buffer: PChar; len: Integer); 你为什么认为它没有使用? - Remy Lebeau

0

不要将PChar转换为string,而应该尝试从地址AInput + (AStart * SizeOf(PChar))复制内存,长度为ASubstringLength * SizeOf(PChar),以@Result作为指针更容易处理Result

Move过程可以实现这一点。


SetString确实就是那样做的,不是吗? - David Heffernan
我的错。我把“SetString”和“UniqueString”搞混了。 - René Hoffmann
我猜SetString检查空终止符,所以这是一个区别。但我的观点是SetString不会创建任何临时字符串,也不会运行整个缓冲区。 - David Heffernan
1
@David,SetString不会检查空终止符。它只是分配一个字符串并调用Move - Rob Kennedy
@Rob 谢谢。我只是在猜测。 - David Heffernan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接