【问题标题】:How can I extract part of a PChar into a string?如何将 PChar 的一部分提取到字符串中?
【发布时间】:2015-06-14 15:57:44
【问题描述】:

在分析过程中,我遇到了一个需要花费大量时间的函数,但基本上可以归结为这段非常简单的代码:

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

此函数返回预期的子字符串,但它不能很好地扩展较长的输入。我在 CPU 视图中查看了汇编程序代码,据我所知(我通常不在汇编程序级别工作),似乎 AInput 在调用 Copy 之前已隐式转换为字符串。

但由于此时字符串/字符数组的长度未知,转换代码必须遍历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 的任何其他警告。注意:FLengthAInput 的实际长度,它来自该函数所属模块的另一部分。我在这个例子中删除了其他部分。

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()
  • “除了 SetString 工作从零开始(不是从一作为复制)” - SetString() 没有索引,句号。它对起始指针而不是索引进行操作。 Copy() 有一个从 1 开始的索引参数,因此原来的 GetSubstring() 也有一个从 1 开始的AStart。您最初修改的GetSubstring() 正在计算错误的指针。它需要使用-1 而不是+1 来保持与Copy() 相同的语义(不包括极端情况):SetString(Result, Ainput + (AStart - 1), ASubstringLength);
  • @RemyLebeau 你是对的。我过度简化了CopySetString 之间的区别。此外,由于我无法访问我的工作机器,我不得不从内存中复制代码,这导致 +1 而不是 -1 和 SetString 函数/过程混淆。现在修复了。
  • @LURD 有趣的是,在内联该方法后,它所花费的时间大约是非内联版本的两倍。不知道为什么会这样,以及它是否与我测量它的方式有关;微基准测试充满了问题。但是,不扫描整个字符串并每次都复制它就足以提高性能。

标签: string delphi optimization pchar


【解决方案1】:

让我们考虑极端情况。我认为他们是:

  1. AInput 无效。
  2. AStart &lt; 1
  3. AStart &gt; FLength
  4. ASubstringLength &lt; 0
  5. ASubstringLength + (AStart-1) &gt; FLength

在我看来,我们可以忽略案例 1。调用者有责任提供有效的PChar。事实上,在我看来,您对AInput &lt;&gt; nil 的检查已经走得太远了,因为nil 不是有效的PChar

其余部分您已经介绍了 2 和 5,但没有介绍 3 和 4。因此,如果用户提供的 AStart 值太大,那么您将读取字符串的末尾。同样,用户可以很容易地提供否定的ASubstringLength。我认为您不需要任何人编写代码来检查这些情况,因为您显然非常有能力。

现在,如果您真的关心每一次性能下降,那么您不应该检查任何这些情况。要求用户传递有效参数。在调试模式下,您可以使用{$IFOPF D+}Assert 检查输入。当然,如果这些论点来自外部来源,那么它们应该得到验证。

另一方面,原始代码遭受的最大性能损失是对整个字符串的不必要扫描,以及复制到中间堆分配的字符串。一旦您删除了这些,就像您所做的那样,进一步提高性能的机会就会大大减少。

【讨论】:

  • 其实代码IS使用System.SetString()函数:procedure SetString(var s: string; buffer: PChar; len: Integer);为什么你认为不是?
【解决方案2】:

与其将PChar 转换为string,不如尝试将长度为ASubstringLength * SizeOf(PChar) 的内存从地址AInput + (AStart * SizeOf(PChar)) 复制到@Result,因为将Result 作为指针处理要容易得多。

Move 程序可以做到这一点。

【讨论】:

  • SetString 正是这样做的,不是吗
  • 我的错。我将SetStringUniqueString 混淆了。
  • 我猜 SetString 检查空终止符,所以这是一个区别。但我的观点是 SetString 不会创建任何临时字符串,也不会在整个缓冲区中运行。
  • @David, SetString 不检查空终止符。它只是分配一个字符串并调用Move
  • @Rob 谢谢。我只是猜测。
猜你喜欢
  • 2017-09-18
  • 1970-01-01
  • 2020-12-18
  • 1970-01-01
  • 2020-11-08
  • 1970-01-01
  • 2021-08-26
相关资源
最近更新 更多