社区所有版块导航
Python
python开源   Django   Python   DjangoApp   pycharm  
DATA
docker   Elasticsearch  
aigc
aigc   chatgpt  
WEB开发
linux   MongoDB   Redis   DATABASE   NGINX   其他Web框架   web工具   zookeeper   tornado   NoSql   Bootstrap   js   peewee   Git   bottle   IE   MQ   Jquery  
机器学习
机器学习算法  
Python88.com
反馈   公告   社区推广  
产品
短视频  
印度
印度  
Py学习  »  Python

性能媲美 Rust,语法丝滑如 Python ,这门国产编程语言凭什么?

码农翻身 • 16 小时前 • 25 次点击  

写过数据处理程序的同学,应该都有过这样的经历。


需求看起来很简单,就是接收一条消息,检查几个字段,再交给后面的业务逻辑。


真正写起来,却没那么轻松。


字段可能缺失,类型可能不对,有些值需要补默认值,还有些数据根本不是JSON,而是一段文本、一串字节。


业务逻辑没写几行,检查和转换先写了一大堆。


等功能终于跑通,数据量上来,又发现了新的问题:字符串复制了太多次,小对象不断创建,一个循环要逐字节执行几百万次。


这时,我们希望手里的语言既能把规则写清楚,也能让人看见并控制这些成本。


MoonBit 在这方面的设计,我觉得值得展开聊聊。


它提供的模式匹配、View、值类型和 SIMD,看起来是几个独立功能,但放进具体程序里,就能看见它们之间的联系。


先从一条最普通的购买记录说起。



01
一条 JSON,为什么要检查这么多遍?


假设服务收到这样的数据:


{  "type": "purchase",  "sku": "book-001",  "amount": 99,  "currency": "CNY"}


程序要处理它,首先得确认:


这是不是购买事件?商品编号有没有传?金额是不是数字?币种是不是字符串?


这些检查并不复杂,但如果拆散了写,往往就变成一串字段读取、类型判断和错误分支。


MoonBit 可以直接把要求写进一个模式:


enum Event {  Purchase(StringDoubleString)derive(Debug)fn decode_event(value : Json) -> Event? {  match value {    {      "type""purchase",      "sku"String(sku),      "amount"Number(amount, ..),      "currency"String(currency),      ..    } => Some(Purchase(sku, amount, currency))    _ => None  }}


即使第一次看 MoonBit,也能大致读懂:


符合这个结构,就取出商品编号、金额和币种,构造一个 Purchase;不符合,就返回 None。


其中,.. 表示允许存在其他不关心的字段。


看到这里的便利了吗?


数据应该长什么样,匹配成功后要拿走什么,写在了同一个地方。


转换完成以后,后面的代码就可以围绕 Purchase 工作,不用反复检查原始 JSON。



图 1 从外部 JSON 到内部类型,先检查结构,再进入后续逻辑。图中仅展示部分输入字段。


如果币种允许缺省,把上面模式中的 "currency": String(currency) 替换为下面这段:


"currency"? :  Some(String(currency)) |  (None with currency = "CNY")


有字符串币种,就使用传入值;没有这个字段,就默认使用人民币。


缺失与类型错误也区分开了。传入一个数字,不会被悄悄当成“没填”。


这类写法的价值,在于减少数据检查和转换中的机械操作。


至于商品是否存在、订单是否重复,仍然要交给相应的业务规则。


把结构检查做好,业务代码才能更专注地处理这些问题。




02
数据变成一串字节,代码还能看懂吗?


JSON 至少还有字段名,如果输入是一条二进制消息,摆在程序面前的,就只是一串字节。


假设一种消息头占三个字节:


第一位固定为 1,接下来三位表示消息类型,再后面四位表示版本,最后两个字节按大端序表示负载长度。


手工解析当然可以,先移位,再做掩码,取出几个字段,还得注意字节序。


问题是,过两个月再读这段代码,你可能需要重新拿出协议文档,对着那些数字算一遍。


MoonBit 的位段模式,可以把这些规则直接写出来:


struct FrameHeader {  kind : UInt  version : UInt  payload_length : UInt} derive(Debug)
fn parse_header(input : BytesView) -> FrameHeader? {  match input {    [       u1be(1),      u3be(kind),      u4be(version),      u16be(payload_length),      ..    ] => Some({ kind, version, payload_length })    _ => None  }}


u3be(kind),就是按模式指定的方式取出三位,绑定到 kind。


u16be(payload_length),就是取出十六位,按大端序解释为负载长度。


代码的排列顺序,基本就是协议字段的排列顺序。


以后协议变了,至少能比较直接地看出哪里需要调整。


这里仅解析消息头;负载是否完整、长度是否合法,还需要后续检查。


从 JSON 字段检查到二进制协议解析,MoonBit 都允许把数据形状和取值规则写在一起。


这解释了 MoonBit 在数据处理上的表达能力。


接下来的问题是:这样写,会不会让程序多做很多事情?



03
同一个解析函数,既描述规则,也控制成本


回头看刚才的函数,它接收的参数是:


input : BytesView


这里的 BytesView,表示已有字节数据的一段视图。


假设一条消息已经读进内存,解析函数只需要其中的消息头。使用视图,可以直接读取这段范围,不必为了传入三个字节,再创建一个独立的字节数组。


有点像读书时标出一段文字的起止位置,不用每次都把那段文字复印一份。


这样,刚才的代码就同时做到了两件事:


用模式描述怎样解释数据,用视图避免为读取这段数据额外复制一份。


表达方式和性能成本,在这个函数里接上了。


继续看返回值,FrameHeader 只有三个整数。如果程序大量创建这类短小对象,性能分析又确认存在分配压力,就可以考虑把它声明为值类型:

#valtypestruct FrameHeader {  kind : UInt  version : UInt  payload_length : UInt} derive(Debug)

#valtype 为符合条件的结构体提供按值表示的方式,可以减少相应的堆分配。


业务代码仍然用 FrameHeader 表示消息头,优化时则进一步调整它的表示方式。


现在,把这个函数从头到尾看一遍:


模式负责把协议规则写清楚,BytesView 让解析器直接读取已有数据,值类型则为解析结果提供进一步减少分配的机会。这里控制的是具体环节的成本,不代表整个解析函数零复制、零分配。



图 2 在同一段解析逻辑中,用模式描述规则,用 View 和值类型控制相应成本。字段宽度仅作示意。


没有必要为了控制复制和分配,把协议解析全部拆回一长串底层操作。


当然,视图和值类型都需要结合场景使用。例如长期保存一个小视图,可能让它引用的较大底层数据继续存活。


它们提供了选择,是否值得采用,要看具体成本。




04
一个字节一个字节处理,能不能再快一点?


再看一种常见任务:在数据中寻找换行符。


普通写法很好理解,取一个字节,比较一次,再处理下一个。


如果性能分析发现,大量时间确实花在这个循环里,就可以评估批量处理。


MoonBit 的 V128 可以表示一个 128 位向量。把它看作十六个字节通道,就可以对这些通道执行相同的比较操作。


以下用实验性的 V128 接口演示批量比较思路,具体写法和支持情况需要与所用工具链版本对应。对于已经取得的一个十六字节块,核心处理可以写成:


let newline = @v128.i8x16_splat(b'\n')let matches = @v128.i8x16_eq(block, newline)let mask = @v128.i8x16_bitmask(matches)


这三步分别是:把换行符填入各个通道,比较对应位置,再把结果收集成一个整数位掩码。


比如第二个和第四个字节是换行符,掩码的低四位就是:1010


后续代码可以利用这个结果定位换行位置。



图 3 匹配通道产生 FF;提取各通道最高位后,得到对应的位掩码。


取得数据块,也可以和前面介绍的模式结合起来:位段模式支持用 v128le 提取十六个字节,构成一个 V128 值。


这样,模式负责匹配并取出数据块,向量操作负责批量比较。


完整扫描器还需要循环处理后续内容,以及最后不足十六字节的部分。一次比较十六个字节,也不等于整个程序快了十六倍。


但它展示了另一种衔接方式:清楚的数据处理结构,可以与局部的批量计算配合。


开发者不必在整个程序里都想着向量指令,只需要在确认值得优化的地方使用它。



0 5
写清楚之后,怎么把性能优化坐下去


回头看前面的消息解析,我们最初的要求其实很简单:把数据读对,把规则写清楚。


模式匹配让字段要求和取值逻辑放在一起。等数据量上来,需要进一步优化时,我们又可以检查:读取消息头时是否发生了多余复制?大量解析结果是否带来了分配压力?扫描循环是否占用了太多时间?


这些问题,分别对应着前面介绍的 View、值类型和 SIMD。


不过,提供了优化手段,并不意味着换一种写法就一定更快。前面的示例展示了这些能力怎样配合,具体能带来多少收益,还需要在相同输入、相同处理结果下,对优化前后的实现进行测量。


如果瓶颈在数据库,优化字节扫描可能收效有限;如果时间主要花在复制和分配上,就应该先检查数据在程序里怎样流动。


MoonBit 在这里值得关注的地方,是写清楚数据处理规则之后,开发者仍然有机会继续调整关键环节的成本。


先把程序写对、写清楚,再找到最值得优化的地方,最后用测试检查效果。


对日常开发来说,这样的路径,比一个脱离具体场景的性能数字更有参考价值。




06
好写与高效,怎样走到一起?


以前面那个消息解析函数为例,模式让协议格式容易阅读,视图让输入可以复用已有存储,值类型又为短小结果提供了另一种表示方式。需要处理计算热点时,还可以继续引入批量读取和 SIMD。


这些能力放在一起,体现了 MoonBit 在这方面的设计思路:


让开发者用较高层的方式描述数据,同时保留进一步控制运行成本的空间。


写第一版时,把规则表达清楚;性能瓶颈出现后,仍能在同一门语言里继续控制复制、分配和批量计算。这是 MoonBit 把表达力与性能连接起来的实际价值。


这些选择能带来多少收益,最终还要由具体项目的测量结果回答。

Python社区是高质量的Python/Django开发社区
本文地址:http://www.python88.com/topic/201065