金融界2025年8月1日消息,国家知识产权局信息显示,安徽林洪重工科技有限公司申请一项名为“深海采油设备用钢的制备方法”的专利,公开号CN120400...
2025-08-02 0
本来呢,Golang(以下简称 Go,与平台同名)是一门相对新颖、有点特色、有活力的编程语言。但是呢,Go 粉丝里总有些黑粉,破化力极强。众所周之,Go 粉丝爱 Go 之情溢于言表。夸一夸自己喜爱的平台、语言,正常操作,无可厚非。然而、却有黑粉到处碰瓷,一抬一踩。甚至于,虚假宣传,不惜用明显错误的程序假造性能测试!(我没有怀疑某些人数据造假,已经是很客气了。依在下的观感,Github 等社区是很少有人在性能方面吹虚 Go 的。)
这是流传比较广的一个例子,我来揭露一下。
测试环境:
我故意把测试代码写得很相像,方便大家对比。上代码。
Go
package mainimport ( "flag" "fmt" "os" "sync" "time")func measure(start time.Time, name string) { elapsed := time.Since(start) fmt.Printf("%s took %s", name, elapsed) fmt.Println()}var maxCount = flag.Int("n", 1000000, "how many")func f(output, input chan int) { output <- 1 + <-input}func test() { fmt.Printf("Started, sending %d messages.", *maxCount) fmt.Println() flag.Parse() defer measure(time.Now(), fmt.Sprintf("Sending %d messages", *maxCount)) finalOutput := make(chan int) var left, right chan int = nil, finalOutput for i := 0; i < *maxCount; i++ { left, right = right, make(chan int) go f(left, right) } right <- 0 x := <-finalOutput fmt.Println(x)}var wg sync.WaitGroupfunc test1() { defer wg.Done() test()}func testPal() { var cout = os.Stdout var runs = 10 fmt.Printf("Started, Running %d tests.", runs) fmt.Println() // var fakecout, _ = os.Open("/dev/null"); var fakecout, _ = os.Open("NUL") os.Stdout = fakecout wg.Add(runs) defer measure(time.Now(), fmt.Sprintf("Running %d tests", runs)) for i := 0; i < runs; i++ { go test1() } wg.Wait() os.Stdout = cout}func main() { test() test() fmt.Println() time.Sleep(1000 * time.Millisecond) testPal() fmt.Println()}
Java(待补)
C#
#nullable enableusing System;using System.Collections.Generic;using System.IO;using System.Linq;using System.Threading.Channels;using System.Threading.Tasks;using System.Threading;using System.Collections.Concurrent;using System.Diagnostics.CodeAnalysis;// [assembly: CLSCompliant(true)]namespace ChannelsTest { using time = UltimateOrb.Ported.Go.StandardLibraries.time.Module; public class Program { static int maxCount; static async Task measure(DateTimeOffset start, string name) { var elapsed = time.Since(start); Console.Write("{0} took {1}", name, elapsed); Console.WriteLine(); } static async Task f(ChannelWriter<int> output, ChannelReader<int> input) { await output.WriteAsync(1 + await input.ReadAsync()); } static async Task test() { await using var Go = new Go(); Console.Write("Started, sending {0} messages (Channel).", maxCount); Console.WriteLine(); Go.defer(measure, time.Now(), string.Format("Sending {0} messages (Channel)", maxCount)); var finalOutput = Channel.CreateUnbounded<int>(); (Channel<int>? left, var right) = (null, finalOutput); for (var i = 0; i < maxCount; i++) { (left, right) = (right, Channel.CreateUnbounded<int>()); _ = f(left, right); } await right.Writer.WriteAsync(0); var x = await finalOutput.Reader.ReadAsync(); Console.WriteLine(x); } static async Task f1(TaskCompletionSource<int> output, TaskCompletionSource<int> input) { output.SetResult(1 + await input.Task); } static async Task test1() { await using var Go = new Go(); Console.Write("Started, sending {0} messages (TaskCompletionSource).", maxCount); Console.WriteLine(); Go.defer(measure, time.Now(), string.Format("Sending {0} messages (TaskCompletionSource)", maxCount)); var finalOutput = new TaskCompletionSource<int>(); (TaskCompletionSource<int>? left, var right) = (null, finalOutput); for (var i = 0; i < maxCount; i++) { (left, right) = (right, new TaskCompletionSource<int>()); _ = f1(left, right); } right.SetResult(0); var x = await finalOutput.Task; Console.WriteLine(x); } static async Task<int> f2(Task<int> input) { return 1 + await input; } static async Task test2() { await using var Go = new Go(); Console.Write("Started, sending {0} messages (Task).", maxCount); Console.WriteLine(); Go.defer(measure, time.Now(), string.Format("Sending {0} messages (Task)", maxCount)); var initialInput = new TaskCompletionSource<int>(); (Task<int>? left, var right) = (null, initialInput.Task); for (var i = 0; i < maxCount; i++) { (left, right) = (right, f2(right)); } initialInput.SetResult(0); var x = await right; Console.WriteLine(x); } static async Task runTest(Func<Task> test) { await Task.Delay(1000); await test(); await test(); Console.Out.WriteLine(); } static async Task runTestPal(Func<Task> test, string? label = default) { await Task.Delay(1000); { using var Go = new Go(); label = null == label ? "" : string.Format(" ({0})", label); var runs = 10; Console.Write("Started, Running {0} tests{1}.", runs, label); Console.WriteLine(); var cout = Console.Out; var fakecout = new StreamWriter(Stream.Null); Console.SetOut(fakecout); var a = new Task[runs]; Go.defer(measure, time.Now(), string.Format("Running {0} tests{1}", runs, label)); for (var i = 0; i < a.Length; i++) { a[i] = Task.Run(test); } await Task.WhenAll(a); Console.SetOut(cout); } Console.WriteLine(); } static async Task Main(string[] args) { if (!int.TryParse(args.FirstOrDefault(), out maxCount)) maxCount = 1000000; await runTest(test); await runTest(test1); await runTest(test2); await runTestPal(test, "Channel"); await runTestPal(test1, "TaskCompletionSource"); await runTestPal(test2, "Task"); } } public struct Go : IDisposable, IAsyncDisposable { Stack<Func<Task>> _defereds; Stack<Func<Task>> defereds { get => _defereds ??= new Stack<Func<Task>>(); } public void defer(Func<Task> defered) { defereds.Push(defered); } public void defer<T>(Func<T, Task> defered, T arg) { defereds.Push(() => defered(arg)); } public void defer<T1, T2>(Func<T1, T2, Task> defered, T1 arg1, T2 arg2) { defereds.Push(() => defered(arg1, arg2)); } public void defer<T1, T2, T3>(Func<T1, T2, T3, Task> defered, T1 arg1, T2 arg2, T3 arg3) { defereds.Push(() => defered(arg1, arg2, arg3)); } public void defer<T1, T2, T3, T4>(Func<T1, T2, T3, T4, Task> defered, T1 arg1, T2 arg2, T3 arg3, T4 arg4) { defereds.Push(() => defered(arg1, arg2, arg3, arg4)); } public void Dispose() { DisposeAsync().AsTask().Wait(); } public async ValueTask DisposeAsync() { var defereds = _defereds; if (defereds is null) { return; } List<Exception>? exs = null; for (; defereds.Count > 0;) { var defered = defereds.Pop(); try { await defered(); } catch (Exception ex) { (exs ??= new List<Exception>()).Add(ex); } } if (exs is not null) { throw new AggregateException(exs); } } }}
Go 运行结果
版本:go1.16.3 windows/amd64
Started, sending 1000000 messages.1000000Sending 1000000 messages took 4.6661903sStarted, sending 1000000 messages.1000000Sending 1000000 messages took 679.7071msStarted, Running 10 tests.Running 10 tests took 13m14.7316935s
是的,你没看错。花了 13 分钟。原因是内存爆炸了。
CPU、内存占用简直离谱!
我实在不忍看下去了。错过了峰值的截图。
可以想见,服务器上的其他程序会受到极严重性能负面影响。
内存不爆掉话,目测要 10 秒左右,还不错。(哪位同学有 120+ GB 内存主机的可以一试。回头请告诉我结果。)这种激进的线程/协程调度和怠惰的 GC 支撑起的性能「优势」(较短运行时间)是以极高的资源消耗为代价的。这就人们大多数时候都选择不这么做的原因。
Java 运行结果(待补)
C# 运行结果
版本:6.0.100-preview.2.21155.3
Started, sending 1000000 messages (Channel).1000000Sending 1000000 messages (Channel) took 00:00:03.5163491Started, sending 1000000 messages (Channel).1000000Sending 1000000 messages (Channel) took 00:00:02.9645047Started, sending 1000000 messages (TaskCompletionSource).1000000Sending 1000000 messages (TaskCompletionSource) took 00:00:00.4585285Started, sending 1000000 messages (TaskCompletionSource).1000000Sending 1000000 messages (TaskCompletionSource) took 00:00:00.7226047Started, sending 1000000 messages (Task).1000000Sending 1000000 messages (Task) took 00:00:00.3038359Started, sending 1000000 messages (Task).1000000Sending 1000000 messages (Task) took 00:00:00.2955279Started, Running 10 tests (Channel).Running 10 tests (Channel) took 00:00:21.2640695Started, Running 10 tests (TaskCompletionSource).Running 10 tests (TaskCompletionSource) took 00:00:03.9964616Started, Running 10 tests (Task).Running 10 tests (Task) took 00:00:02.0500174
这份运行结果,还是在受到刚刚 Go 的余波影响跑出来的。
怎么还有额外的内存 uncommit 不掉…… 可耻的 Windows。
我们看到,内存消耗只有 Go 的 1/10 左右。累计 CPU 时间是差不多的。C# 的 CPU 占比只有 Go 的三分之一左右,但时间跨度约是 Go 的两倍半。由于没有受到内存不足的惩罚,10x 并发测试中,只花了 20 秒左右就完成了任务。总体而言,.NET 的调度更加平稳,GC 更加勤快。这对高并发是为极有利的。
重启系统以后的运行结果:
Started, sending 1000000 messages (Channel).1000000Sending 1000000 messages (Channel) took 00:00:03.0937316Started, sending 1000000 messages (Channel).1000000Sending 1000000 messages (Channel) took 00:00:03.0468371Started, sending 1000000 messages (TaskCompletionSource).1000000Sending 1000000 messages (TaskCompletionSource) took 00:00:00.5690865Started, sending 1000000 messages (TaskCompletionSource).1000000Sending 1000000 messages (TaskCompletionSource) took 00:00:00.4801076Started, sending 1000000 messages (Task).1000000Sending 1000000 messages (Task) took 00:00:00.3199227Started, sending 1000000 messages (Task).1000000Sending 1000000 messages (Task) took 00:00:00.2920232Started, Running 10 tests (Channel).Running 10 tests (Channel) took 00:00:18.8371428Started, Running 10 tests (TaskCompletionSource).Running 10 tests (TaskCompletionSource) took 00:00:03.5347325Started, Running 10 tests (Task).Running 10 tests (Task) took 00:00:01.8442157
平均数值上稍稍好了一点点,主要是稳定多了。
如果使用 TaskCompletionSource 或 Task 在协程间传递消息,则 C# 在 CPU 占比、内存消耗和运行时间三方面都完胜 Go。这样比不公平,但另一方面 .NET 也可以用定制的 Channel。能达到同等运行时间,且 CPU 占比和内存消耗均低于 Go(说明并发能力高),后续文章我会进一步介绍。 不过这里重点不在乱比性能,而是要明白各种配置的优劣,配合任务性质的才是好的。
对于一般业务场景,.NET 协程(Task/ValueTask)的综合性能远高于 Go 协程(Goroutine)。这是多个因素导致的。其中一个重要原因是 Go 协程内存消耗大,也就有更多缓存没命中,甚至要换页进出内存。
Goroutine 本质上是一种优化过的有栈协程。因此具备有栈协程对无栈协程的优势。有栈协程赋予同步过程中 await 异步协程的能力。无栈协程没有这种功能,一般需要利用操作系统功能阻塞等待。(顺带一提,不依赖操作系统的解法主要有三类。一是 Emscripten 中的 Asyncify,其原理是调用堆栈的 unwind 和 rewind(rewind 是重点),其中算法类似于结构化异常处理中算法,性能开销较大,实现繁琐,但适用性最广。二是自旋/轮询,简单粗暴,但性能开销极大,且不适用于单线程执行模型。三是 V8 的 %PerformMicrotaskCheckPoint,功能受限且仅适用于单线程执行模型。)
Go 不需要 async/await。我们注意到,Go 的 async 函数和正常函数没有区别。而无栈协程往往要将 async 函数编译为状态机。这就是 Go 不需要 async/await 的真正原因。即既无需 async 指示编译器将哪些函数编译为状态机。又(站在无栈协程的角度)所有 await 都是隐式的,也不需要指明。剩下的唯一情况是子过程的 detach(非 join) 模式执行。这正好是 go 关键字的用途,说穿了,毫无神奇之处。这对应于 C# 中无 await 调用 async 函数,并直接 ignore 返回的 Task 对象,或传给 Task.Run 。(对比示例代码。)
.NET 提供的 Channel API 存在影响性能的设计缺陷,Go 至少没犯这种低级错误。最重要的是 Go 的 chan 有语言整合程度的支持,用起来非常便捷。C# 的代码啰嗦多了。Java 就更啰嗦了。
defer 语法糖不错。(可惜当年,大家讨论过以后 C# 没有加,我在社区里说反对加,现在稍稍有点后悔。)
Channel = Buffer + Semaphore
此处 Buffer 一般用 Queue。看清了这个,就知道怎么把 Channel 当 Semaphore 用了。哈哈。(但是性能肯定比直接用 Semaphore 要差。)
Semaphore 上比较难做文章,这里就吐槽一下。同时支持同步等待和异步等待(指使用 await 等待)的 Semaphore 实现起来稍稍麻烦。大多数语言/平台提供的默认实现只支持一种等待方式。JavaScript 的这个 Semaphore 支持异步等待。C# 的经典版 Semaphore 支持同步等待(SemaphoreSlim 两者都支持)。Go 的 chan 和 sync.WaitGroup(= Semaphore)可以认为支持两者(有栈协程特性)。
Buffer 的部分花样就比较多了。优化得好可以提速不少。特别是观察到,大多是业务场景里,Channel 的数据不会积存,能存几个元素的 Buffer 就够用。这时可不另设堆上空间,随 Channel 存放即可。
首先,谨慎使用 go 关键字。明白原理后(虽然本文没有写),我们能发现,没有必要为 IO 型任务启动新的 goroutine。应当在任务具有一定的计算载荷,且需要运行一段时间(例如 100 ms 以上)时,才使用 go 启动新协程。这样也要求我们尽可能避免引发阻塞的调用。(如果库的实现是阻塞的,那就只能被迫妥协了。)
有栈协程的其他缺点也要注意。例如,因为用 Task 封装异步结果不是必须的。需要这种功能是就需要用 chan 或 sync 里类型了(例如:示例代码里借助 WaitGroup)。但我们知道,这比 Task 的性能要差。所以,可以考虑将 API 设计为批处理(batching/chunky)的形式,避免零碎的调用,发挥 Channel 的真正优势。或在 hot path 中用更基础的同步原语实现。
Go 的标准库并没有提供一套可取消 API,这可谓是一大缺憾。因为理论上讲,有栈协程更需要避免阻塞调用,可取消 API 则是一大利器。一定规模以上的长期项目,应该自行准备一套可取消 API 的支持库。(先把支持库备好,接口改好,不是要立即写好尊重取消 token 状态的业务代码实现。)长远来看,收益很大。既然大家都开源,我建议从 .NET 好好抄一部分来。
调度策略激进与否,对性能测试除了有直接影响还有间接影响。例如,激进调度和自旋锁跑满 100% CPU,CPU 不降频。而正常调度和内核可等待对象,一般跑不满 100% CPU(否则早被人骂死了),CPU 可能会降频。一来二去,拉开运行时间的差距,但并不能说明前者更好。没有免费的午餐啊!
支持调用系统同步 API 的语言/平台是不可能需要 async/await all the way 的,例如 C#。污蔑啊。实际上,测试代码里就有例子。Task 是有 Wait 方法可供调用的。退一万步,还有 ContinueWith 和 then(例如 JavaScript)。
服务程序用 Go 开发,往往意味着要花更多钱在服务器上!当然,这对像 Google 那样占据舆论高地的云服务提供商来说,真是天大的好事!
这篇文章,还原真相外,目的让大家用好 Go,别被带偏了。(当然,我说的也不是全部真相。我建议大家自己动手试一试,找出真相,不要人云亦云的。)但毕竟扫了一些人的兴,甚至影响了一些人的财路。有些作者说得比较委婉,但这不是我的风格。反正我有些憋不住,不吐不快。欢迎在评论区留言讨论。(请尽情给出建议和指出本文错漏之处。)
相关文章
金融界2025年8月1日消息,国家知识产权局信息显示,安徽林洪重工科技有限公司申请一项名为“深海采油设备用钢的制备方法”的专利,公开号CN120400...
2025-08-02 0
本来呢,Golang(以下简称 Go,与平台同名)是一门相对新颖、有点特色、有活力的编程语言。但是呢,Go 粉丝里总有些黑粉,破化力极强。众所周之,G...
2025-08-02 0
金融界2025年8月1日消息,国家知识产权局信息显示,陕西万科电子科技发展有限公司取得一项名为“一种火炬燃烧器顶部稳焰块的制作工装”的专利,授权公告号...
2025-08-02 0
近日,美国《华盛顿邮报》刊登了一篇关注中国能否赢下与美国在人工智能(AI)领域竞争的文章。文章认为,中国和美国采取的是两条不同的方案:当美国在用最尖端...
2025-08-02 0
在淄博周村,传统工业区的烙印正在被悄然改写,一场由智能制造引领的深刻变革已不再是未来蓝图,而是车间里真实跳动的脉搏。在弗徕威机器人产业园嘉视(山东)电...
2025-08-02 0
苹果即将推出备受期待的 iPhone 17 Pro 和 Pro Max 系列新机,从外观革新到影像系统的大幅升级,这一次的变化绝不仅仅是“挤牙膏”。以...
2025-08-02 0
虽说接下来的红米K90很香,但其起售价肯定不会低于2499,而且想要出现打骨折的价格,肯定是明年618的事情了,距离现在还远着呢。反观红米K80目前1...
2025-08-02 0
7月30日中国东方航空在空客汉堡引进1架A321neo注册号为B-32LW并于30-31日经停阿斯塔纳成功飞抵上海浦东入役至此东航机队规模增至667架...
2025-08-02 0
发表评论