类 TaskFlow 框架性能年终 PK
开场白
临近元旦,又到了各个平台发年度报告的时间。刚好,离 ograph 第一次正式提交也过了差不多一年了,是骡子是马,咱也趁着这个热闹,拉出来溜溜。
还是先介绍一下 ograph 吧,这是一个基于DAG图的任务流程执行框架。你可能听说过 taskflow,ograph 就是类似的框架。基本的使用方法如下:
func TestHello(t *testing.T) {
pipeline := ograph.NewPipeline()
zhangSan := ograph.NewElement("ZhangSan").UseFn(func() error {
fmt.Println("hello")
return nil
})
liSi := ograph.NewElement("LiSi").UseFn(func() error {
fmt.Println("hi")
return nil
})
pipeline.Register(zhangSan).
Register(liSi, ograph.Rely(zhangSan))
if err := pipeline.Run(context.TODO(), nil); err != nil {
t.Error(err)
}
}
ograph 会按依赖关系,先执行 ZhangSan
输出 hello
, 再执行 LiSi
,输出 hi
。
除了 UseFn
,按实际使用场景或者风格偏好,还可以使用 UseNode
,UseFactory
,这里就不详细展开了。
PK 嘉宾
作为老牌大哥,taskflow 的后来者当然不止 ograph。虽然风格和侧重场景有所不同,但是核心功能是一样的。
下面揭晓本次 PK 嘉宾:
首先是 CGraph🫡🫡🫡
想必你也发现了,ograph 名字和 cgraph 很相似。是的,ograph 确实在风格上 部分借鉴了 CGraph。当然在底层实现上还是完全不同的,毕竟一个是 C++ 写的,一个是 Go 写的。
和一个多人开发,已经经过多年打磨的 C++ 项目比拼性能,还是挺有挑战性的,但也正因是因为这份挑战驱使我一次次优化性能。且看 ograph 是否能后来居上!
下一位来到战场的是:go-taskflow 🤟🤟🤟
想必聪明的你又发现了,go-taskflow 和 taskflow 名字很相似,风格上两者确实也很类似。
这个框架是我最近发现的,也是单人开发了差不多一年的 go 框架,从这点来说和 ograph 还挺像的。既然这么巧碰上了,那就不打不相识,一起来 PK 下吧。
至于为什么没有 taskflow 本尊呢?因为 CGraph 已经有和 taskflow 的性能测试对比了。从测试结果看,CGraph 性能已经领先 taskflow 了。有兴趣的小伙伴可以参考下,CGraph 作者的博客和B站视频:
炸裂!CGraph性能全面超越taskflow之后,作者却说他更想… - 一面之猿网
www.bilibili.com/video/BV1gwWAekEA...
PK 开始
这次 PK 使用的测试用例沿用 CGraph 作者的设计,是对框架核心调度性能的测试。
废话不多说,PK 这就开始。
Round One
第一回合,考验各个框架的并发处理能力。
具体场景如下,有 32 个节点,这些节点之间没有任何依赖关系。因此在执行时候,这些节点是并发执行的。
每个框架各自执行 50 万次。
No.1 OGraph
总耗时 2.17s
BenchmarkConcurrent_32-8 500000 4331 ns/op 1592 B/op 16 allocs/op
PASS
ok github.com/symphony09/ograph/test 2.170s
No.2 CGraph
总耗时 4.13s
**** Running test-performance-01 ****
[2024-12-28 15:33:44.174] [test_performance_01]: time counter is : [4134.87] ms
No.3 go-taskflow
总耗时 9.71s
BenchmarkC32-8 500000 19425 ns/op 7084 B/op 208 allocs/op
PASS
ok github.com/noneback/go-taskflow 9.716s
Round Two
第二回合,考验各个框架对串行节点的执行优化。
具体场景如下,有 32 个节点,前后依次依赖(N1->N2->……->N32),因此在执行时候,节点是一个接一个执行。
每个框架各自执行 100 万次。
No.1 OGraph
总耗时 0.29s
BenchmarkSerial_32-8 1000000 285.9 ns/op 120 B/op 4 allocs/op
PASS
ok github.com/symphony09/ograph/test 0.290s
No.2 CGraph
总耗时 0.57s
**** Running test-performance-02 ****
[2024-12-28 15:33:44.749] [test_performance_02]: time counter is : [572.61] ms
No.3 go-taskflow
总耗时 67.3s ?
go-taskflow 跑 100 万次时,出现卡死的情况。只能取跑 10 万次的耗时再乘 10。
BenchmarkS32-8 100000 67309 ns/op 6907 B/op 255 allocs/op
PASS
ok github.com/noneback/go-taskflow 6.735s
Round Three
第三回合,考验各个框架对经典 DAG 图的执行性能。
具体场景如下:
各个框架各执行 100 万次
No.1 OGraph
总耗时 2.778s
BenchmarkComplex_6-8 1000000 2773 ns/op 1048 B/op 21 allocs/op
PASS
ok github.com/symphony09/ograph/test 2.778s
No.2 CGraph
总耗时 4.027s
**** Running test-performance-03 ****
[2024-12-28 15:33:48.778] [test_performance_03]: time counter is : [4027.31] ms
No.3 go-taskflow
总耗时 8.223s
BenchmarkC6-8 1000000 8219 ns/op 1264 B/op 45 allocs/op
PASS
ok github.com/noneback/go-taskflow 8.223s
Round Four
第四回合,考验各个框架对极多依赖的优化。
具体场景如下:8x8 全连接
各个框架各执行 10 万次
No.1 OGraph
总耗时 0.86s
BenchmarkConnect_8x8-8 100000 8557 ns/op 2554 B/op 16 allocs/op
PASS
ok github.com/symphony09/ograph/test 0.860s
No.2 CGraph
总耗时 1.357s
**** Running test-performance-04 ****
[2024-12-28 15:33:50.137] [test_performance_04]: time counter is : [1357.03] ms
No.3 go-taskflow
总耗时 7.35s ?
又出问题了,故技重施
BenchmarkC8x8
[panic] copool: node N24 ref counter is zero, cannot deref: goroutine 248743
结算
我宣布,ograph 《遥遥领先》😎😎😎。如果你也觉得 OK 的话,请给 ograph 一个 Star!
symphony09/ograph: 【Go DAG Schedule Framework】A simple way to build a pipeline with Go.
附录
测试硬件
【CPU】AMD 5600G - 6 核心 12 线程
【内存】DDR4 32 GB
测试软件
【系统】Linux 6.11 - Fedora Workstation 41
【OGraph】v0.6.1 - Go 1.23.4
【CGraph】v2.6.2 - GCC14.2.1
【go-taskflow】v0.1.6 (master分支最新提交)
测试代码
【OGraph】
ograph/test/benchmark_test.go at main · symphony09/ograph
【CGraph】
CGraph/test/Performance at main · ChunelFeng/CGraph
【go-taskflow】
func BenchmarkC32(b *testing.B) {
tf := gotaskflow.NewTaskFlow("G")
for i := 0; i < 32; i++ {
tf.NewTask(fmt.Sprintf("N%d", i), func() {})
}
executor := gotaskflow.NewExecutor(32)
for i := 0; i < b.N; i++ {
executor.Run(tf).Wait()
}
}
func BenchmarkS32(b *testing.B) {
tf := gotaskflow.NewTaskFlow("G")
prev := tf.NewTask("N0", func() {})
for i := 1; i < 32; i++ {
next := tf.NewTask(fmt.Sprintf("N%d", i), func() {})
prev.Precede(next)
prev = next
}
executor := gotaskflow.NewExecutor(1)
for i := 0; i < b.N; i++ {
executor.Run(tf).Wait()
}
}
func BenchmarkC6(b *testing.B) {
tf := gotaskflow.NewTaskFlow("G")
n0 := tf.NewTask("N0", func() {})
n1 := tf.NewTask("N1", func() {})
n2 := tf.NewTask("N2", func() {})
n3 := tf.NewTask("N3", func() {})
n4 := tf.NewTask("N4", func() {})
n5 := tf.NewTask("N5", func() {})
n0.Precede(n1, n2)
n1.Precede(n3)
n2.Precede(n4)
n5.Succeed(n3, n4)
executor := gotaskflow.NewExecutor(1)
for i := 0; i < b.N; i++ {
executor.Run(tf).Wait()
}
}
func BenchmarkC8x8(b *testing.B) {
tf := gotaskflow.NewTaskFlow("G")
layersCount := 8
layerNodesCount := 8
var curLayer, upperLayer []*gotaskflow.Task
for i := 0; i < layersCount; i++ {
for j := 0; j < layerNodesCount; j++ {
task := tf.NewTask(fmt.Sprintf("N%d", i*layersCount+j), func() {})
for i := range upperLayer {
upperLayer[i].Precede(task)
}
curLayer = append(curLayer, task)
}
upperLayer = curLayer
curLayer = []*gotaskflow.Task{}
}
executor := gotaskflow.NewExecutor(8)
for i := 0; i < b.N; i++ {
executor.Run(tf).Wait()
}
}
本作品采用《CC 协议》,转载必须注明作者和本文链接