写点什么

我是如何把 5 万行 C++ 代码移植到 Go 的?

  • 2019-04-13
  • 本文字数:2795 字

    阅读完需:约 9 分钟

我是如何把5万行C++代码移植到Go的?

Go 语言的创始人之一 Rob Pike 曾表示,他希望 Go 能够被 C++程序员所接受,但结果差强人意。最近,在作者就职的 HFT 公司里,一个团队成功地把一些对速度不太敏感的基础设施代码从 Python 移植到了 Go,这也促使他们决定尝试用 Go 对复杂冗余的 C++服务端程序进行重构,这些代码有 5W 行之多,并且对吞吐量有一定的要求。

这个服务端程序使用了跟公司核心交易软件相同的技术和库,不同地是交易软件对系统的延迟更敏感,几乎每一微秒都很重要,而 C++服务端并不需要这种程度的性能。

因此,使用 Go 自带的调度程序完全可以满足要求,没有必要使用交易系统实现的超优化 C++框架,虽然损失了一些性能但获得了更好的可维护性。需要一提的是,本文作者负责了整个代码的重写工作。

前言

从商业角度来看,这个项目是成功的:重写工作提前完成;性能在可接受的范围之内;并且整体代码量不超过 1W 行(代码量的剧减主要是因为重写团队删除了一些过时的或者不需要的特性)。但从开发者的角度来看,作者认为结果并不是最优的。Go 并不支持参数多态,作者因此使用了两到三倍的代码来实现类似功能。其中一部分是为了保障类型安全:Go 强制开发者在类型修饰和类型安全之间做出取舍,作者选择了一个比较均衡的实现。总的来说,如果需要一般的类型安全,那么相对少的代码就可以实现,而如果需要更好的类型安全,则需要更多的代码。


接下来让我们对比一下 Go 语言的优缺点。

优点

Emacs 开发平台

借助自动完成、跳转到定义、保存时的错误检查、智能重构和 GoTest 集成等插件,Emacs 成为了 Go 语言环境下最好的 IDE 工具。另外,它也可以很方便地通过 Elisp 进行定制和扩展。如果你本人恰好是 Emacs 的爱好者,这绝对是一个大大的加分项。

Goroutines(协程)

Go 实现了基于消息传递的并发,作者认为这是最简单的并发形式,使用也超级方便。通过将 GOMAXPROCS 设置为 1,Go 还允许开发者通过使用与并发代码完全相同的方式来编写并行/异步代码。与其它提供内置轻量级线程调度器的语言 Erlang/Elixir 和 Haskell 相比:前者缺乏静态类型,后者在实际开发中很少被管理人员采用。

没有继承

在很多情况下,基于继承的 OO(面向对象)是一种反模式,这些冗余和模糊的代码几乎没有什么好处,Go 则直接取消了这类代码。这有可能也是 Rob Pike 等人设计 Go 的初衷:谷歌内部有一大堆类似于企业版本 Fizzbuzz 的 Java/C++代码,他们希望能从这些代码中彻底解放出来。也就是说,尽管在旧的 C++服务端遗留代码中使用继承是合理的,但最好还是使用更现代的风格来重写代码,而且重写过程也并不复杂。

更好的可读性

Go 代码更易于阅读和理解。相比之下,很多 C++代码需要几个小时才能完全理解。Go 本身也促使开发者编写可读的代码:这种语言完全避免了下面这种自做聪明的情形


“嘿,这篇论文(基本上没人读得懂)中的>8=3 运算符可以让我节省 10 行代码,我最好把它写进代码里,我的同事也不难理解这行代码,因为它的意思已经在类型签名中很清楚地表达出来了(反正我是没看懂):(PrimMonad W, PoshFunctor Y, ReichsLens S) => W Y S((I -> W) -> Y) -> G -> Bool "。

简单而规范的语法

当我们需要将一个封闭函数的名称添加到每个日志字符串的开头时,如果使用 Emacs,一个简单的 regexp find-replace(正则表达式)命令就可以实现,而对于更复杂的语言则需要使用解析器。不论是通过 Emacs 宏或者是 Go 模板,简单的语法可以更容易地生成代码。


Emacs+Go==参数多态:我们可以使用 Emacs 宏来加速生成 Go 所需要的"复制粘贴",而且,如果函数编写正确,那我们也可以用 regex 命令来更新所有的"复制粘贴"函数。这样,我们就可以很容易地更新 fooInt、fooFloat 和 fooDouble 等函数,对比支持参数多态的语言对 foo 函数的更新,整个过程没有什么太大区别。这样做的缺点是,虽然 Emacs 宏和 regex 命令可以编写和修改 Go 代码,但它仍然不如真正的多态实现那样简洁和易读;而且对于不熟悉 regex 以及可扩展编辑器(Emacs)的人来说,维护同样也不容易。

有效的内置模板

通过 Go 的文本/模板包,我们可以很容易地生成新代码。它还允许开发者在生成代码时使用 IO:例如,有一个同某些特定服务交互的库,它通过 XML Schema 生成。如果能够用不同的函数来生成不同的数据类型,那么就可以保证代码的类型安全。


在 C++中,IO 不能在编译时执行,因此不能使用上述模式来生成代码。允许编译时使用 IO 的语言有:


  • F#,通过 TypeProviders 实现。

  • Idris,也使有 TypeProviders。

  • Lisp,可以在宏中执行 IO。

  • Haskell,它有一个编译期运行的函数 IO -> Q。

  • D,编译时可以使用“import”来读取文件。

  • Nimrod,有特殊的函数实现。

  • Elixir 或 Erlang,可以通过宏执行任意的 IO。

  • Rust,可以使用函数 libsyntax 在编译时执行任意的计算和 IO。

缺点

斯德哥尔摩综合征

前面已经提到,在允许使用 IO 的特性上,使用模板生成 Go 代码要比用 C++元编程好得多,而 C++元编程在这里显然是多余的,因为完全可以用另外一种可以支持 IO 的程序语言来生成代码。

没有实现参数多态

尽管很多人认为这在实践中并不是一个问题,但在这里,它是一个很严重的问题。如果把新的 Go 代码再移植回 C++的话,考虑到 C++的函数多态和类型多态,代码量可能会减少到目前的一半,并且具有更好的类型安全。如果用 Haskell 重写的话,代码量会更少,而使用 Clojure 的话,代码量有可能控制到 1000 行以内,当然这些代码可能很难被调试或维护。

牺牲了类型安全

针对服务器处理的各种 protobuffer messages(协议缓冲消息),我们使用了扩展属性的方式,作者最初打算为每一种消息设置一种扩展属性,这样 FooExtensionAttribute 就不能用在 Bar 函数上。Go 并没有实现参数多态和泛型,这意味着将会产生大量的重复代码,所以最终只使用了一种 ExtensionAttribute,并且类型系统也没有检查它是否用于扩展合适的消息。

二进制文件太大

如果使用代码来生成类型安全的 API,并确保每种数据类型都有明确的类型访问器和诸如此类的东西,则很容易生成超过 10W 行的 Go 代码以及 30MB 以上的二进制文件,编译时间也会更长。在这种情况下,一般会超过 10 秒。当然,这不是一个很严重的问题,因为我们可以把代码编译成静态库,这只需要一次,之后就可以通过静态链接来访问了。

内核兼容性有待提高

很多时候由于各种无奈的原因,需要把代码部署到一个旧内核上。而且,如果这个内核不支持最新的 Go 版本,就不得不换到一个旧的、很慢的 Go 版本,这多少有些令人沮丧。

结语

Go 语言是一把双刃剑:它禁止一切复杂的抽象,不管是优秀的抽象亦或是很差的抽象。如果你和你的同事正在使用很糟糕的抽象,那切换到不能使用抽象的 Go 语言自然很好,反之亦然。当然这也要取决于判断抽象好坏的标准。


查看英文原文


https://togototo.wordpress.com/2015/03/07/fulfilling-a-pikedream-the-ups-of-downs-of-porting-50k-lines-of-c-to-go/



2019-04-13 14:0016773

评论 3 条评论

发布
用户头像
我心中的编程语言尝鲜指数:
第一梯队:Rust(系统编程),Julia(服务器端开发及数据领域),Dart(移动开发)
第二梯队:C++1x(系统编程),Go(服务器端开发),Python(服务端开发及数据领域),Kotlin(移动开发),Swift(移动开发)
第三梯队:C/传统C++(系统编程),Java(服务器端开发及Android开发),PHP(服务器开发)
2019-04-22 12:21
回复
用户头像
试试Rust吧,效果会更好!
2019-04-21 14:21
回复
用户头像
希望哪一天,go能支持泛型和多态
2019-04-15 11:08
回复
没有更多了
发现更多内容

Apache APISIX 社区双周报 | 11.15-11.30 功能亮点更新进行中

API7.ai 技术团队

开源 云原生 网关 api 网关 Apache APISIX

Go语言学习查缺补漏ing Day1

恒生LIGHT云社区

编程语言 Go 语言

Spark SQL之RDD转换DataFrame的方法

@零度

大数据 RDD DataFrame spark SQL

极光笔记|百亿级KV存储在极光的运维实践之路

极光GPTBots-极光推送

华云大咖说 | 安超信创桌面云金融行业解决方案

华云数据

刚提测就改需求,我是渣男吗?

小傅哥

Java 加班 小傅哥 需求迭代 产品功能

python 爬虫爱好者必须掌握的知识点“ 协程爬虫”,看一下如何用 gevent 采集女生用头像

梦想橡皮擦

12月日更

MySQL only_full_group_by 1055 报错的三种解决方案,临时关闭有影响吗?

蒋川

MySQL 报错 MySQL 数据库

Apache APISIX 2.11.0 正式发布,蓄力两月带来更多新功能!

API7.ai 技术团队

开源 云原生 网关 API网关 Apache APISIX

Redis玩转Message Queue之Stream详述

李子捌

redis 28天写作 Redis Stream 12月日更

Java开发之如何连接Redis

@零度

redis JAVA开发

推开“微前端”的门

百度Geek说

微服务 大前端

大咖联袂发布!《慧技术·惠金融——2022金融科技趋势研究报告》开放下载

恒生LIGHT云社区

金融科技 行业趋势 行业大会

Git进阶(三):webstorm 的 git 切换分支

No Silver Bullet

git 12月日更

【Dart 专题】Generics 泛型 <T>

阿策小和尚

Flutter 小菜 0 基础学习 Flutter Android 小菜鸟 12月日更

如何使用GoldWave软件将文字转换为语音

懒得勤快

Rust 元宇宙 11 —— Websocket

Miracle

rust websocket 元宇宙

Hutool中那些常用的工具类和方法

编程江湖

JAVA开发 java工具包

设计模式【4】-- 建造者模式详解

秦怀杂货店

设计模式

Springboot & RabbitMQ 延时队列的使用

编程江湖

大数据 消息中间件

数字化转型鸿沟如何消除?ROMA Connect融合集成,联接企业应用现在与未来

华为云开发者联盟

多云服务 应用 集成 集成平台 ROMA Connect

运维监控场景下,如何从OpenTSDB迁移到TDengine

TDengine

数据库 tdengine

【12月11日】真香现场,带你玩转 EKS!

亚马逊云科技 (Amazon Web Services)

人工智能 Meetup EKS

六个数字化意识和习惯

明道云

Linux学习方法《Linux一学就会》:重定向和文件的查找

侠盗安全

Rust 元宇宙 从零开始构建

Miracle

rust 元宇宙

前端开发面试题分享,看一下是不是你需要的

@零度

大前端 面试题

迈向云原生:名企FreeWheel应用架构演进

博文视点Broadview

【云小课】CDN第5课 CDN入门之—我的网站可以用CDN加速吗?

华为云开发者联盟

网站 CDN 网络 CDN加速 华为云CDN

最受欢迎的5个React动画库

编程江湖

React

JDK 动态代理与 CGLIB 动态代理,它俩真的不一样

华为云开发者联盟

jdk 动态代理 spring aop JDK 动态代理 CGLIB 动态代理

我是如何把5万行C++代码移植到Go的?_编程语言_logicchains_InfoQ精选文章