Go生成图片水印demo,既然比PHP慢一倍?
闲聊
这个打水印的 demo 其实已经完成许久,一直没有总结总结,有空填了一下自己的坑吧,也让自己复习复习。
背景
公司是做图形设计资源站点,详情、搜索页面都需要提供预览图片,图片都是包含公司的水印的图片,水印图片单独存储。现在公司需要更换水印图,所以要获取全部的原图,打上新水印,再替换现有的图片。
方案
公司主要是 PHP 开发, 本来是打算用 Laravel 的 command 脚本完成的,但想到当时团队也是有意向转 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 的部分截图

找出耗时原因了,原来耗时的都卡在了 png.Decode() 和 jpeg.Encode() 这两步了,再来看看 PHP 这两步的时间
php index.php 总耗时: 55.06ms 水印decode耗时: 0.87ms 原图decode耗时: 24.47ms 新图encode耗时: 19.63ms 所以 Go 比 PHP 慢的原因就是出现在 decode() 和 encode() 这两步了:
PHP在decode两张图片用时 25.34ms,encode 用时 19.63msGo在decode两张图片用时 60 ms,encode 用时 50ms
图片大小和耗时成正比,测试原图是 1.43 M,如果有知道原因的小伙伴可以分享分享
协程
虽然我 decode 和 encode 慢,但我有 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 慢这么多,希望有知道的朋友可以交流交流。
总结
Go提供的工具很方便的找出性能瓶颈,如上文的PProf- 协程的优势非常大,在自身函数较慢的情况下,可以充分利用协程发挥系统性能赶超。
参考
本作品采用《CC 协议》,转载必须注明作者和本文链接
关于 LearnKu
高认可度评论:
总之C写的东西,性能上永远不会过时,我感觉php主要问题是没有强大的公司带,开发方向完全靠开发组内部投票,试想下如果让谷歌接手php项目,协程,异步方面生态的标准应该早就能完善了把
PHP这几个官方函数可是c写的,倒不如感叹C的效率如此之高
php就是c的库,用php就是用c,c的性能
go的特效不在于此
总之C写的东西,性能上永远不会过时,我感觉php主要问题是没有强大的公司带,开发方向完全靠开发组内部投票,试想下如果让谷歌接手php项目,协程,异步方面生态的标准应该早就能完善了把
从文章代码来看,后面对比不太公平吧,PHP 是单进程单核跑,Go 协程在多核上跑。
c还是很牛逼的
那我用 swoole 的协程岂不是更快 🐶
php用上swoole的话有了协程同等情况下应该就会比go快了
php其实就是c的一个大型项目。这个属于计算开销,不是 IO,这个场景下只要 PHP 用多进程模型(基于脚本)肯定是底层为 C 的 PHP 更有优势,用上协程这些也比不了的,Go 性能本来也不是其特色,主要是在对高 IO 开销场景下能充分利用资源
有查看内存和cpu方面开销对比么,好像也不同