Go生成圖片水印demo,既然比PHP慢一倍?

zxr615發表於2022-08-02

閒聊

這個打水印的 demo 其實已經完成許久,一直沒有總結總結,有空填了一下自己的坑吧,也讓自己複習複習。

背景

公司是做圖形設計資源站點,詳情、搜尋頁面都需要提供預覽圖片,圖片都是包含公司的水印的圖片,水印圖片單獨儲存。現在公司需要更換水印圖,所以要獲取全部的原圖,打上新水印,再替換現有的圖片。

方案

公司主要是 PHP 開發, 本來是打算用 Laravelcommand 指令碼完成的,但想到當時團隊也是有意向轉 Go 方向並且Go在有協程的加持,可以開多個協程來執行打水印這部分工作,所以最終採用了 Go 語言來完成這個需求,算是一個的新的嘗試。

實現

整體流程是:遍歷資料庫資料->獲取原圖資訊->下載原圖->生成水印->上傳水印圖->更新資料庫水印圖片,本文主要敘述生成水印此步驟,因為這一塊與 PHP 的實現上略有不同,在 Go 上可使用協程併發處理,而 PHP 只能單個處理。

Go實現

go version go1.17.7 darwin/arm64

同步

同步執行流程與 PHP 一樣,對比一下兩者效率

main.go

// 同步
func syncGen() {
    startT := time.Now()

    // 開啟水印圖
    water, err := pkg.OpenPngImage("./test_water.png")
    if err != nil {
        log.Fatal(err)
    }

    // 這個迴圈是遍歷原圖
    imgPath := "./test.png"
    for i := 1; i <= num; i++ {
        id := i + 1000 // 模擬 id
        if err = pkg.Generate(imgPath, id, water); err != nil {
            log.Println("水印圖生成失敗,id=" + strconv.Itoa(i))
        }
    }

    // 計算耗時
    tc := time.Since(startT)

    fmt.Printf("同步執行時間 = %v\n", tc)
}
go run main.go -n 1
水印圖:1 張
同步執行時間 = 109.644375ms

1632*874 JPEG(24位顏色) 78.87kb

PHP實現

PHP 7.4.28 (cli)

臨時寫了個 PHP 版本的同功能demo

$t1 = microtime(true);

$dst   = '/home/jiumu/code/go-image-water-case/test1.png';
$src   = '/home/jiumu/code/go-image-water-case/test_water1.png';
$font  = '/home/jiumu/code/go-image-water-case/font.ttf';

$t3 = microtime(true);
$srcIm = imagecreatefrompng($src);
$t4 = microtime(true);

$num = 1;
for ($i = 0; $i < $num; $i++) {
    $t5 = microtime(true);
    $dstIm = imagecreatefrompng($dst);
    $t6 = microtime(true);

    $dstSize = getimagesize($dst);
    $srcSize = getimagesize($src);
    $new     = imagecreatetruecolor($dstSize[0], $dstSize[1]);
    imagecopy($new, $dstIm, 0, 0, 0, 0, $dstSize[0], $dstSize[1]);
    imagecopy($new, $srcIm, $dstSize[0] - 200, $dstSize[1] - 220, 0, 0, $srcSize[0], $srcSize[1]);
    $rgb = imagecolorallocate($dstIm, 21, 33, 57);//字型顏色
    $id    = $i + 1000;
    imagefttext($new, 30, 0, $dstSize[0] - 200, $dstSize[1] - 20, $rgb, $font, "ID: {$id}");
    $t7 =  microtime(true);
    imagejpeg($new, __DIR__ . "/water/{$i}.jpeg");
    $t8 =  microtime(true);
    imagedestroy($dstIm);
    imagedestroy($new);
}

imagedestroy($srcIm);
$t2 = microtime(true);

echo '總耗時: ' . round($t2 - $t1, 5). 's' . PHP_EOL;
echo '總耗時: ' . round($t2 - $t1, 5) * 1000 . 'ms' . PHP_EOL;

結果

$ php index.php
總耗時: 0.05506s
總耗時: 55.06ms

1632*874 JPEG(24位顏色) 78.82kb

PHP 既然比Go快了近一倍?難道是我Go的程式碼寫的有問題?後來發現PHP生成的圖片質量只有Go的75%的質量,所以把Go這邊的質量降到了75%,效率依然沒有變化,在一籌莫展的時候,想到了 PProf 找一下效能到底卡在哪裡了。

生成 1 張水印圖 pprof 的部分截圖

image-20220802160747317

找出耗時原因了,原來耗時的都卡在了 png.Decode()jpeg.Encode() 這兩步了,再來看看 PHP 這兩步的時間

php index.php
總耗時: 55.06ms
水印decode耗時: 0.87ms
原圖decode耗時: 24.47ms
新圖encode耗時: 19.63ms

所以 GoPHP 慢的原因就是出現在 decode()encode() 這兩步了:

  • PHPdecode 兩張圖片用時 25.34ms,encode 用時 19.63ms
  • Godecode 兩張圖片用時 60 ms,encode 用時 50ms

圖片大小和耗時成正比,測試原圖是 1.43 M,如果有知道原因的小夥伴可以分享分享

協程

雖然我 decodeencode 慢,但我有 goroutine 啊,來看看 Go 的表現

各生成100張水印圖:

php

總耗時: 5.34911s
總耗時: 5349.11ms

go

NumCPU:8

NumGoroutine: 102

$ go run main.go -n 100
水印圖:100 張
同步執行時間 = 9.583812958s
無併發數量控制時間 = 2.072805143s

協程狀態下比 PHP 快了 2.5 倍,同步情況下,還是比php慢2倍。

協程控制

由於啟動的 goroutine 協程不受控制,如果panic了則無法處理,成為野生攜程,導致程式掛掉。

// Go 避免 go func(){} 如果方法中丟擲 panic 無法被捕獲到
// 或者是每在每個 go 前面都 recover() 一次,造成的程式碼混亂不可維護
func Go(f func()) {
    defer func() {
        if err := recover(); err != nil {
            // 記錄日誌
            log.Println(err)
        }
    }()

    go f()
}

開啟協程的方式則變成:

pkg.Go(func() {
  // code
})

由於程式碼篇幅較長,所以程式碼沒有貼出來,有興趣的小夥伴可以到此檢視原始碼 github.com/zxr615/go-image-water-c...

Go 的 decode / encode 為何相對 php 慢這麼多,希望有知道的朋友可以交流交流。

總結

  1. Go 提供的工具很方便的找出效能瓶頸,如上文的 PProf
  2. 協程的優勢非常大,在自身函式較慢的情況下,可以充分利用協程發揮系統效能趕超。

參考

《Go 語言高效能程式設計》pprof 效能分析

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章