跳转到正文

并行工作进程

Greph 会在工作量足以抵消开销时,将文本和 AST 扫描分派到工作进程池。该进程池基于 pcntl_fork() 构建;如果 ext-pcntl 不可用(Windows、某些 PaaS 环境或受限托管环境),则回退到进程内执行。

启用并行扫描

在 CLI 上设置 -j N,或在相关选项对象中设置 jobs: N:

命令行
./vendor/bin/greph -F -j 8 "function" src
./vendor/bin/greph -p '$obj->$method($$$ARGS)' -j 4 src
./vendor/bin/rg --threads 8 -F "function" src
./vendor/bin/sg run --threads 8 --pattern '$obj->$method($$$ARGS)' src
PHP
use Greph\Greph;
use Greph\Text\TextSearchOptions;

Greph::searchText(
    'function',
    'src',
    new TextSearchOptions(fixedString: true, jobs: 8),
);

并行何时启动

Greph 并非总会并行运行。启动 N 个进程并通过管道往返传递结果需要开销;对于较短的文件列表,这些开销会超过搜索本身的成本。因此,门面会根据作业数量、搜索模式和模式形状计算阈值。只有文件列表超过阈值时,才会分派到工作进程池。

当前启发式规则(位于 Greph\Greph::shouldUseTextWorkers 及相关组件中):

  • 单作业运行(-j 1)始终在进程内执行。
  • 默认文本扫描需要大约 jobs * 2_000 个文件才会启用进程池。
  • 计数、包含匹配项的文件和不包含匹配项的文件模式会将阈值降低到大约 jobs * 750,因为每个文件的工作量较少。
  • 对于较短、类似标识符的固定字符串模式,阈值会提高到大约 jobs * 4_000,因为字面量快速路径已经非常快,启动进程很少划算。
  • AST 搜索和 AST 重写使用 jobs * 750 阈值。

这些数字是启发式设定,并非保证值。它们根据 WordPress 基准测试语料调优,之后可能会重新调整。

进程池的工作方式

Greph\Parallel\WorkSplitter 会将文件列表分成 N 个块。Greph\Parallel\WorkerPool 会为每个块派生一个工作进程;每个工作进程对其文件切片运行相同的闭包,Greph\Parallel\ResultCollector 则通过每个工作进程对应的 Unix 套接字对读取序列化结果。父进程会等待所有工作进程完成,然后合并并重新排序结果集,确保无论工作如何拆分,输出顺序都保持稳定。

文本模式使用自定义的线路编码器(Greph\Text\TextResultCodec),以缩小序列化载荷。AST 模式使用 PHP 的标准 serialize()。

pcntl 不可用时

如果 pcntl_fork 不可调用(Windows、经过强化的 PHP 构建),进程池会透明地回退为在进程内运行每个块。结果与 -j 1 相同。Greph 不会报错,只会以单线程运行。

你可以在运行时通过检查 function_exists('pcntl_fork') 来检测这一点。

调优建议

  • 将 -j 设为物理核心数的一半,然后逐渐提高,直到性能不再提升。
  • 对小型代码库进行纯字面量固定字符串搜索时,单进程通常比任何并行配置都快。
  • 没有已发布的基准测试评估并行 AST 搜索或重写。在确定作业数前,请在自己的目录树上进行测量。
  • README 中标为“扫描模式:并行文本”的行(在 WordPress 语料上使用 1、2 和 4 个工作进程)并未测量进程池的性能:它们搜索的是较短的字面量 function,其 jobs * 4_000 个文件的阈值高于语料规模,因此每一行实际上都在进程内运行(#8)。

限制

  • 当前进程池不支持工作窃取。如果一个块的负载远重于其他块,进程池会等待最慢的工作进程。
  • 工作进程会将结果缓存起来,直到所有进程完成。并行模式不支持长时间扫描时的流式输出(如 rg --json 流式传输事件的方式)。
  • 父进程会在合并结果后重新排序,因此与单进程扫描相比,工作进程池不会降低峰值内存用量。

对于 Greph 的目标工作负载(交互式代码搜索、代理循环,以及针对快照运行的 CI 作业)而言,这些限制可以接受。如果性能分析显示它们在实际语料中形成瓶颈,我们会重新评估。