并行工作进程
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
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 作业)而言,这些限制可以接受。如果性能分析显示它们在实际语料中形成瓶颈,我们会重新评估。