写点什么

我是如何把 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:0016500

评论 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
回复
没有更多了
发现更多内容

读完这份阿里高质量性能优化全解,我的系统终于不再频繁瘫痪了

Java 阿里巴巴 编程 面试 金九银十

中国移动5G消息开发者社区第三期直播课堂圆满结束,直播回放已上线社区!

5G消息

和腾讯大佬的技术面谈,BTAJ面试有关散列(哈希)表的面试题详解

Java 程序员 后端

基础+缓存雪崩+哨兵+集群+Reids场景设计,经验分享

Java 程序员 后端

桐乡引入百度智能云开物工业互联网,数字经济再提速

百度大脑

人工智能

人类视觉神经科学助力音视频产业革命 - 弱网下的极限实时通信

声网

音视频 弱网下的极限实时视频通信

【Vuex 源码学习】第一篇 - Vuex 的基本使用

Brave

源码 vuex 9月日更

突击 22 天面进腾讯,给到 32K*14 薪!全靠这份阿里面试参考指南了

Java 程序员 架构 面试 计算机

分布式事务内存数据库--MemDB

hanaper

自定义View笔记

Changing Lin

9月日更

要不要换种方式开发软件?

鲸品堂

软件开发

和阿里大牛的技术面谈,springcloud面试题汇集与答案

Java 程序员 后端

OceanBase 源码解读(四):事务的一生

OceanBase 数据库

数据开发 oceanbase OceanBase 开源 OceanBase 社区版 OceanBase 数据库大赛

如何让项目准时上线 - 续篇

石云升

项目管理 管理 引航计划 内容合集 9月日更

译介:《组装一台电脑9:精简》

姬翔

9月日更

FLV格式解析

轻口味

android 音视频 RTMP 9月日更

NoSQL数据库——Cassandra

hanaper

上云迁移之路,如何选择适合方式?

云计算

和腾讯大牛的技术面谈,分布式系统中ACID和CAP有什么区别

Java 程序员 后端

想要掌握未来嘛?这份腾讯高工撰写的Redis实战笔记就告诉你什么是未来!

Java 腾讯 面试 大厂 金九银十

MDEX市值机器人系统开发功能介绍

量化系统19942438797

交易所 市值机器人 MDEX

和阿里大牛的技术面谈,字节跳动Java实习面试凉凉经

Java 程序员 后端

关系型数据库如何存储树形结构?

王博

什么?分布式事务现在不是都在用么?你还不会?

Java 架构 分布式 后端 计算机

一年数十万次实验背后的架构与数据科学

百度开发者中心

人工智能 架构 最佳实践 方法论 数据科学

JavaScript进阶(上)

Augus

JavaScript 9月日更

堪比狗血剧!18名Java程序员凭阿里P8笔记,同时斩获大厂offer

Java 编程 面试 阿里 大厂

网络安全之一个渗透测试小案例

网络安全学海

网络安全 信息安全 渗透测试 WEB安全 安全漏洞

和阿里大牛的技术面谈,金三银四旗开得胜

Java 程序员 后端

复盘上次Redis缓存雪崩事故,中级Java工程师面试题

Java 程序员 后端

Nacos 开源、自研、商业化三位一体战略解读

阿里巴巴中间件

云计算 阿里云 微服务 云原生 nacos

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