前置学习

如果我问你,PHP拓展的执行效率比PHP原生代码更高吗? 你会不假思索的回答我。是的。 但是,为什么呢?为什么用C/C++开发的拓展效率就一定高呢?

如果你问Chat-GPT4,它会回答:

编译型语言:PHP扩展通常使用C或C++编写,这些都是编译型语言,编译成机器码后执行效率高,可以直接操作硬件和内存。
直接操作内核数据结构:PHP扩展可以直接访问和操作PHP内核的数据结构和API,避免了PHP脚本中一些额外的运行时开销,如变量查找、类型判断、内存分配等。
优化的算法和数据结构:使用C或C++编写的PHP扩展可以利用更优化的算法和数据结构,例如使用高效的哈希表、树、图等数据结构,或者使用并行计算、矢量化计算等优化的算法。
系统级操作:PHP扩展可以直接调用操作系统的API进行系统级的操作,比如文件操作、网络操作、进程管理等,这些操作通常比PHP脚本中的相应操作要高效。

个人总结:程序执行阶段,拓展比应用层语言更贴近操作系统。 PHP可以理解成是一款用C/C++开发的应用,这款应用语法简单,编程效率高,对应的代价便是运行效率低。 为了深入理解这个问题,让我们一起来先来深入学习一下PHP这门编程语言的体系架构。

拓展的优点:

  • 优化性能,尤其是计算密集型的逻辑
  • 代码保护

PHP System

图片

  • Zend Engine:它是PHP的核心,负责解析和执行PHP脚本。所有的PHP代码最终都会被Zend Engine解析成字节码(opcode),然后执行。
  • PHP Extension:这是通过Zend API编写的模块,它们可以直接访问和操作Zend Engine的内部数据结构和函数,提供各种PHP函数和类给PHP脚本使用。例如,PDO、mysqli等数据库扩展,GD图形处理扩展等。 注意⚠️:PHP Extension 和 Zend Extension 不是一个东西
  • Zend API 和 Zend Extension API 都是Zend Engine提供的接口,专门用于PHP编程语言和PHP Extension调用。 通过它,外部模块可以访问和操作Zend Engine的内部数据结构和函数。
  • PHP SAPI(Server API):这是PHP和外部环境(如Web服务器)之间的接口。不同的SAPI实现可以让PHP运行在不同的环境中,例如Apache模块(mod_php)、CGI、FastCGI、CLI(命令行接口)等。

PHP Execution Flow

图片

  • Scanning(词法分析):PHP源代码首先会被分解为一系列的标记(token)。这一过程就是词法分析。PHP源代码被读入并转化为一系列的标记,比如变量名、函数名、关键字(如if, else等)、操作符等。 Lex生成的,源文件在 Zend/zend_language_sanner.l
  • Parsing(语法分析):接下来,这些标记会被组织成一种结构化的形式,通常是一棵抽象语法树(AST)。这一过程就是语法分析。在这个阶段,PHP会检查代码的语法是否正确,比如括号是否匹配,语句是否完整等。 yacc生成, 源文件在 Zend/zend_language_parser.y
  • Compilation(编译):紧接着,抽象语法树会被转化为opcode。这一过程就是编译。编译过程中,PHP会进行一些优化,比如常量折叠(constant folding)、死代码消除(dead code elimination)等。 opcode定义的源文件在zend_vm_opcodes.h
  • Execution(执行):最后,opcode会以op array的形式被Zend Engine顺序执行,完成实际的运算和操作。在这个阶段,PHP会进行函数调用、变量查找、表达式求值等操作。

在PHP 8中增加的JIT编译器是作为Opcache的一部分提供的,JIT在Opcache优化后的基础上,结合运行时信息将热点代码(频繁执行的代码)编译成机器码,从而进一步提高执行速度。

Token

<?php
var_dump(token_get_all('<?php
$name = "PHP";
echo "$name is the best language in the world.";
echo PHP_EOL;'));

AST

pecl install ast
<?php
var_dump(ast\parse_code('<?php
$name = "PHP";
echo "$name is the best language in the world.";
echo PHP_EOL;', 70));

Opcodes

pecl install vld
<?php
$name = "PHP";
echo "$name is the best language in the world.";
echo PHP_EOL;
php -dvld.active=1 ./test.php
Finding entry points
Branch analysis from position: 0
1 jumps found. (Code = 62) Position 1 = -2
filename:       /Users/admin/www/swoole-test/vld_test.php
function name:  (null)
number of ops:  6
compiled vars:  !0 = $name
line      #* E I O op                           fetch          ext  return  operands
-------------------------------------------------------------------------------------
    2     0  E >   ASSIGN                                                   !0, 'PHP'
    3     1        NOP
          2        FAST_CONCAT                                      ~2      !0, '+is+the+best+language+in+the+world.'
          3        ECHO                                                     ~2
    4     4        ECHO                                                     '%0A'
   24     5      > RETURN                                                   1

branch: #  0; line:     2-   24; sop:     0; eop:     5; out0:  -2
path #1: 0,
PHP is the best language in the world.

我们先来详细的看一下opcode表格里每一列的内容具体代表什么

  • line:这是源代码中的行号。例如,第一个 opcode 对应的源代码在第 2 行。
  • #*:这是 opcode 的序号。例如,ASSIGN 是第 0 个 opcode。
  • E 和 I、O:表示 opcode 的执行入口和跳转出口。
  • op:这是 opcode 的类型。例如,ASSIGN、NOP、FAST_CONCAT、ECHO 和 RETURN。
  • fetch 和 ext:这两列提供了 opcode 的额外信息。在这个例子中,这些列为空。
  • return:这列表示 opcode 的返回值。例如,FAST_CONCAT 的返回值是 ~2。
  • operands:这是 opcode 的操作数。例如,ASSIGN 的操作数是 !0 和 ‘PHP’,FAST_CONCAT 的操作数是 !0 和 ‘+is+the+best+language+in+the+world.’。
  • 具体到每个 opcode:
  • 第 0 个 opcode ASSIGN:将字符串 ‘PHP’ 赋值给变量 !0(即 $name)。
  • 第 1 个 opcode NOP:不进行任何操作,通常用于占位或者标记。
  • 第 2 个 opcode FAST_CONCAT:将变量 !0 和字符串 ‘+is+the+best+language+in+the+world.’ 进行连接,结果保存在 ~2 中。
  • 第 3 个 opcode ECHO:输出 FAST_CONCAT 的结果 ~2。
  • 第 4 个 opcode ECHO:输出字符串 ‘%0A’(即换行符)。
  • 第 5 个 opcode RETURN:返回 1,表示脚本执行成功。

(以上来自Chat-GPT4的回答)

学习开发一款PHP拓展

下面我们使用两种方式来开发一款PHP拓展,这个拓展的功能就是很简单,就实现一个字符串反转函数。

php -v
PHP 7.3.33 (cli) (built: Jun 17 2023 07:00:43) ( NTS )
Copyright (c) 1997-2018 The PHP Group
Zend Engine v3.3.33, Copyright (c) 1998-2018 Zend Technologies
with Zend OPcache v7.3.33, Copyright (c) 1999-2018, by Zend Technologies

使用C开发拓展

使用PHP官方提供的脚本生成拓展骨架

cd php-src/ext
php ./ext_skel.php --ext funplus

声明 && 实现函数方法

我们准备在funplus拓展里实现一个简单的字符串翻转方法,函数参数是一个字符串,返回值是翻转后的字符串。 在php_funplus.h声明函数方法

PHP_FUNCTION(funplus_reverse_string);

在funplus.c注册方法、实现方法逻辑

/* {{{ arginfo
 */
ZEND_BEGIN_ARG_INFO(arginfo_funplus_reverse_string, 0)
ZEND_ARG_INFO(0, str)
ZEND_END_ARG_INFO()
/* }}} */

/* {{{ funplus_functions[]
 */
static const zend_function_entry funplus_functions[] = {
    PHP_FE(funplus_test1, arginfo_funplus_test1)
        PHP_FE(funplus_test2, arginfo_funplus_test2)
            PHP_FE(funplus_reverse_string, arginfo_funplus_reverse_string)
                PHP_FE_END};
/* }}} */

// arginfo_funplus_test1 和 arginfo_funplus_test2 是骨架自带的


/* {{{ string funplus_reverse_string( [ string $var ] )
 */
PHP_FUNCTION(funplus_reverse_string)
{
    char *var;
    size_t var_len;
    zend_string *retval;

    ZEND_PARSE_PARAMETERS_START(1, 1)
    Z_PARAM_STRING(var, var_len)
    ZEND_PARSE_PARAMETERS_END();

    retval = zend_string_alloc(var_len, 0);
    for (size_t i = 0; i < var_len; i++)
    {
        ZSTR_VAL(retval)
        [i] = var[var_len - i - 1];
    }
    ZSTR_VAL(retval)
    [var_len] = '\0';

    RETURN_STR(retval);
}
/* }}}*/

编译 && 安装拓展

// funplus拓展源码路径
phpize
./configure
sudo make && sudo make install

和PHP原生代码的性能对比

<?php
const IntervalTimes = 1000000;
$var = "";

$start = microtime(true);
for ($i=0 ;$i<IntervalTimes; $i++) {
$var = funplus_reverse_string("42tPJzmsqSbHvVRUGTrapCi4A3mmaohK");
}
var_dump("Extension Total time:" . (microtime(true) - $start));


$start = microtime(true);
for ($i=0 ;$i<IntervalTimes; $i++) {
$var = reverseString("42tPJzmsqSbHvVRUGTrapCi4A3mmaohK");
}
var_dump("PHP Total time:" . (microtime(true) - $start));


function reverseString($str): string
{
$reversed = "";
for ($i = strlen($str) - 1; $i >= 0; $i--) {
$reversed .= $str[$i];
}
return $reversed;
}

//string(38) "Extension Total time:0.038073062896729"
//string(30) "PHP Total time:1.0140709877014"

使用Zephir开发拓展

With Zephir, you can implement object-oriented libraries/frameworks/applications that can be used from PHP, gaining important seconds that can make your application faster while improving the user experience.

安装

pecl install zephir_parser

OR

Bash composer global require "phalcon/zephir:0.17.0"

注:需要把zephir的可执行程序加载到$PATH中 验证是否安装成功

生成拓展骨架

zephir init foo

编写拓展代码

// foo/foo/str.zep
namespace Foo;

class Str
{
public static function bar(string! text) -> string
{
var reversedText, i, len;
let reversedText = "";
let len = strlen(text);

for i in range(len - 1, 0, -1) {
let reversedText .= chr(text[intval(len - i - 1)]);
}

return reversedText;
}
}

编译 && 测试

zephir build
<?php

const IntervalTimes = 1000000;
$var = "";

$start = microtime(true);
for ($i = 0; $i < IntervalTimes; $i++) {
$var = Foo\Str::bar("42tPJzmsqSbHvVRUGTrapCi4A3mmaohK");
}
var_dump("Foo Extension Total time:" . (microtime(true) - $start));


$start = microtime(true);
for ($i = 0; $i < IntervalTimes; $i++) {
$var = reverseString("42tPJzmsqSbHvVRUGTrapCi4A3mmaohK");
}
var_dump("PHP Total time:" . (microtime(true) - $start));


function reverseString($str): string
{
$reversed = "";
for ($i = strlen($str) - 1; $i >= 0; $i--) {
$reversed .= $str[$i];
}
return $reversed;
}

// string(42) "Foo Extension Total time:0.072835922241211"
// string(31) "PHP Total time:0.91381096839905"

使用PHP-CPP开发拓展

The PHP-CPP library is a C++ library for developing PHP extensions. It offers a collection of well documented and easy-to-use classes that can be used and extended to build native extensions for PHP. The full documentation can be found on http://www.php-cpp.com .

在我本机MacOS的支持不好,make编译不通过,卒。

使用PHP-X开发拓展 (By 韩天峰)

git clone https://github.com/swoole/PHP-X.git
cd console && \
echo "composer update" && \
composer update && \
cd ../ && \
php -d phar.readonly=off script/pack.php --disable-gz
sudo cp bin/phpx /usr/local/bin
phpx create cpp_ext

也卒😅

PHP-FFI - 一种使用PHP代码调用C库的方式

For PHP, FFI opens a way to write PHP extensions and bindings to C libraries in pure PHP. 是的,FFI提供了高级语言直接的互相调用,而对于PHP来说,FFI让我们可以方便的调用C语言写的各种库。 其实现有大量的PHP扩展是对一些已有的C库的包装,比如常用的mysqli, curl, gettext等,PECL中也有大量的类似扩展。而有了FFI以后,我们就可以直接在PHP脚本中调用C语言写的库中的函数了。

<?php
const CURLOPT_URL = 10002;
const CURLOPT_SSL_VERIFYPEER = 64;

$libcurl = FFI::cdef(<<<CTYPE
void *curl_easy_init();
int curl_easy_setopt(void *curl, int option, ...);
int curl_easy_perform(void *curl);
void curl_easy_cleanup(void *handle);
CTYPE
, "libcurl.so"
);

$url = "https://www.laruence.com/2020/03/11/5475.html";

$ch = $libcurl->curl_easy_init();
$libcurl->curl_easy_setopt($ch, CURLOPT_URL, $url);
$libcurl->curl_easy_setopt($ch, CURLOPT_SSL_VERIFYPEER, 0);

$libcurl->curl_easy_perform($ch);

$libcurl->curl_easy_cleanup($ch);

参考

PHP 7 Virtual Machine
PHP 运行机制与原理 – 怼码人生
Table Of Contents — PHP Internals Book
深入浅出PHP(Exploring PHP) - 风雪之隅
深入理解PHP原理之Opcodes - 风雪之隅 解析PHP原生扩展开发 - 掘金 Registering and using PHP functions — PHP Internals Book 如何使用C++开发PHP扩展(下) - WiFeng的博客 用C++开发PHP扩展 Zephir Documentation