PHP FFI呼叫go,居然比go還快
上一篇文章中用PHP的FFI成功了呼叫了cjieba,但是速度實在是慢,4個函式迴圈呼叫20次,用了居然1分50多秒,而且C版本只比PHP快一點點,看來是cjieba本身慢了。
這次發現了一個golang的分詞庫gse,試試匯出為動態庫,用FFI載入。
碰到的問題
不能匯出go指標
由於之前對cgo不熟悉,以為go可以很方便的匯出到C,沒想到一開始就把我難倒。
panic: runtime error: cgo result has Go pointer
不能匯出go結構體
一開始直接在go裡返回了[]string,沒想到報錯了,原來go不允許匯出含有指標的資料結構
Go type not supported in export: struct
後來想,要不匯出[]string 的指標,但是如果只有指標地址,沒有長度,遍歷肯定會出錯,於是構造了一個結構體,儲存指標地址和長度,沒想到還是不行。
這期間,由於工作忙(主要是懶),斷斷續續的看了一下cgo相關的內容,先跑通了C呼叫go,於是再試著用FFI,很快也跑通了。
go匯出C動態庫的簡單說明
在go裡,匯出一個函式到C動態庫,其實非常簡單,需要在import C
包,並在匯出的函式加上export 函式名
,加上一個空的main 函式即可,如:
package main import ( "C" ) //必須和函式同名 //export PlusOne func PlusOne(num int) int { return num + 1 } func main() { }
編譯方法如下:
go build -buildmode=c-shared -o libdemo.so demo.go
就會自動生成 so 和 libdemo.h 標頭檔案,開啟libdemo.h,可以看到裡面是各種go 資料型別的定義,摘除部分如下:
typedef signed char GoInt8; typedef unsigned char GoUint8; typedef short GoInt16; typedef unsigned short GoUint16; typedef int GoInt32; typedef unsigned int GoUint32; typedef long long GoInt64; typedef unsigned long long GoUint64;
可以看到還包含了一個另外的標頭檔案#include <stddef.h>
,可以使用gcc -E -P libdemo.h -o libdemo_unfold.h
展開stddef.h合併到一個到標頭檔案,然後複製我們需要的型別定義即可。
PHP FFI呼叫go
## PHP如何初始化go型別變數
由於go的string,slice匯出後,都是一個結構體,不是一個簡單型別,這裡我們先看看string。
typedef struct { const char *p; ptrdiff_t n; } GoString;
可以看到string有一個char* 指標,和一個表示長度的n,可以說明go的string是不帶'\n'的,和C的字串不同。然而一開始,我居然還特意加了'\n',然後給n也加1,結果發現不對,在go那邊加上輸出後,才發現出錯了。
對於這種結構體,用載入動態庫的FFI例項呼叫 new 方法。即$goStr = $ffi->new('GoString',0)
,注意new的第二個引數要傳0,表示這個物件PHP不用管理記憶體。在這個地方,我又掉坑裡了。
然後要給p和n賦值,對於n,比較簡單,直接給字串長度,但是對於p,就比較麻煩。
翻看PHP文件,發現有個memcpy方法,於是試了一下,成功的實現了PHP和go之間傳string。
完整的程式碼如下:
function makeGoStr(FFI $ffi, string $str): FFI\CData
{
$goStr = $ffi->new('GoString', 0);
$size = strlen($str);
$cStr = FFI::new("char[$size]", 0);
FFI::memcpy($cStr, $str, $size);
$goStr->p = $cStr;
$goStr->n = strlen($str);
return $goStr;
}
FFI 靜態方法和FFI例項方法的區別
在上面的程式碼裡,既有FFI的靜態方法,也有例項方法,它們之間的區別在於,靜態方法只有常用的資料型別,如果int,char;例項方法,才能呼叫載入的so裡面的型別。
FFI的三種呼叫思路
下面我說一下三種呼叫思路,建議第一種,這裡就不貼程式碼了,完整的程式碼看github。
1 通過 C.char
由於go不能返回slice string,那麼換個思路,把陣列拼接成字串,然後返回C.char。這種方式最簡單,而且在後面的跑分測試裡發現,也是最有效率的。
2 通過slice 指標傳引數
既然不能返回,那麼我們修改傳入的引數是否可以呢。通過測試發現確實可行。
3 返回指標的地址
這就是一開始我的想法,這種方法有點麻煩,而且速度也不佔優。
跑分測試
可以下載我github的程式碼,對於go需要開啟go mod。
先make lib
,生成go的動態庫,然後make php_test
和make go_test
檢視對比。
go的
TestCut: goseg_test.go:18: CutChar 2000 次用時:41.511794ms
TestCut: goseg_test.go:26: CutPointer 2000 次用時:45.24684ms
TestCut: goseg_test.go:34: CutSlice 2000 次用時:42.537337ms
php的
CutChar 2000 次用時:0.027052 s
CutSlice 2000 次用時:0.038451 s
CutPointer 2000 次用時:0.038257 s
可以發現php居然比go的還快,比cjieba快了不知道多少倍,看來以後一些耗CPU的方式,可以用go來開發動態庫,給PHP用,比通過介面呼叫可以快很多。
假如go的介面5ms,PHP這邊收到請求解析1ms,這樣2000次就是 6s了。可以發現FFI是介面呼叫的0.03/6 = 0.005,是幾百倍數量級的提升。
當然前提是選擇一個性能高的FFI外部庫才行,如果比PHP還慢,那就不必了。
另外FFI可以預載入,鳥哥的部落格寫的很詳細了,大家可以去看看。