普通视图

发现新文章,点击刷新页面。
昨天以前程序员的喵

Future 的大小对性能的影响

作者 yukang
2025年3月24日 18:21

在 Rust 异步编程中,有一种观点认为:Future 的大小显著影响性能。你是否怀疑过这个说法的真实性?如果是真的,这种性能差异的根源又是什么?今天,我翻阅了一些源码,并编写实验代码来一探究竟。

Future 的大小如何计算?

为了验证“Future 大小影响性能”这一说法是否成立,我们先从一些简单代码入手。首要任务是弄清楚一个 Future 的大小是如何确定的。毕竟,在编译器眼里,Future 只是一个 trait:

pub trait Future {    type Output;    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output>;}

那么,其大小取决于实现这个 trait 的具体结构体吗?我翻阅了 smol 的源码,发现在 spawn 一个 Future 时,相关代码是这样处理的:

pub unsafe fn spawn_unchecked<'a, F, Fut, S>(    self,    future: F,    schedule: S,) -> (Runnable<M>, Task<Fut::Output, M>)where    F: FnOnce(&'a M) -> Fut,    Fut: Future + 'a,    S: Schedule<M>,    M: 'a,{    // Allocate large futures on the heap.    let ptr = if mem::size_of::<Fut>() >= 2048 {        let future = |meta| {            let future = future(meta);            Box::pin(future)        };        RawTask::<_, Fut::Output, S, M>::allocate(future, schedule, self)    } else {        RawTask::<Fut, Fut::Output, S, M>::allocate(future, schedule, self)    };    let runnable = Runnable::from_raw(ptr);    let task = Task {        ptr,        _marker: PhantomData,    };    (runnable, task)}

这里可以看到 mem::size_of::<Fut>() 是在计算这个 Future 的大小,我来写个简单的 Future 验证:

use async_executor::Executor;use futures_lite::future;use std::future::Future;use std::pin::Pin;use std::task::{Context, Poll};pub struct LargeFuture {    pub data: [u8; 10240],}impl Future for LargeFuture {    type Output = usize;    fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Self::Output> {        let value = self.data[0];        println!("First byte: {}", value);        Poll::Ready(self.data.len())    }}fn main() {    let ex = Executor::new();    let large_future = LargeFuture { data: [0u8; 10240] };    let res = future::block_on(ex.run(async { ex.spawn(large_future).await }));    println!("Result: {}", res);}

在上面那个 async-task 的 spawn_unchecked 函数加上日志,打印出来的大小为 10256,刚好比这个 struct 的大小大 16,顺着代码往上可以看到这里在原始的 Future 上做了一个封装,这里的意思是如果这个 Future 以后执行完,需要从 runtime 里面删掉:

let future = AsyncCallOnDrop::new(future, move || drop(state.active().try_remove(index)));

这解释了尺寸略有增加的原因。对于结构体的尺寸,我们不难理解,但对于 async 函数,其大小又是如何计算的呢?这就涉及 Rust 编译器对 async 的转换机制。

异步状态机:冰山之下的庞然大物

当你写下一个简单的 async fn 函数时,Rust 编译器在幕后悄然完成了一场复杂的转换:

async fn function() -> usize {    let data = [0u8; 102400];    future::yield_now().await;    data[0] as usize}

这段代码会被编译器转化为一个庞大的状态机,负责追踪执行进度并保存所有跨越 .await 点的变量。转换后的结构体封装了状态切换的逻辑:

enum FunctionState {    // 初始状态    Initial,    // yield_now 挂起后的状态,必须包含所有跨 await 点的变量    Suspended {        data: [u8; 102400], // 整个大数组必须保存!    },    // 完成状态    Completed,}// 2. 定义状态机结构体struct FunctionFuture {    // 当前状态    state: FunctionState,    // yield_now future    yield_fut: Option<YieldNow>,}impl Future for FunctionFuture {    // 3. 为状态机实现 Future traitimpl Future for FunctionFuture {    type Output = usize;    fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<usize> {        // 安全地获取可变引用        let this = unsafe { self.get_unchecked_mut() };        match &mut this.state {            FunctionState::Initial => {                // 创建大数组及其长度                let data = [0u8; 102400];                // 创建 yield future 并保存                this.yield_fut = Some(future::yield_now());                // 状态转换,保存所有需要跨越 await 的数据                this.state = FunctionState::Suspended { data };                // 立即轮询 yield                match Pin::new(&mut this.yield_fut.as_mut().unwrap()).poll(cx) {                    Poll::Ready(_) => {                        // 如果立即完成,返回结果                        if let FunctionState::Suspended { data } = &this.state {                            let result = data[0] as usize;                            this.state = FunctionState::Completed;                            Poll::Ready(result)                        } else {                            unreachable!()                        }                    }                    Poll::Pending => Poll::Pending,                }            }            FunctionState::Suspended { data } => {                // 继续轮询 yield                match Pin::new(&mut this.yield_fut.as_mut().unwrap()).poll(cx) {                    Poll::Ready(_) => {                        // yield 完成,读取数组首元素并返回                        let result = data[0] as usize;                        this.state = FunctionState::Completed;                        Poll::Ready(result)                    }                    Poll::Pending => Poll::Pending,                }            }            FunctionState::Completed => {                panic!("Future polled after completion")            }        }    }}

可以看到,Suspended 状态中包含了那个大数组。当状态从 Initial 切换到 Suspended 时,data 会被完整保留。

由此可知,对于一个 async 函数,若临时变量需跨越 await 存活,就会被纳入状态机,导致编译时生成的 Future 大小显著增加。

尺寸对性能的影响

明确了 Future 大小的定义后,我们接着通过代码验证其对性能的影响。在之前的 mem::size_of::<Fut>() >= 2048 条件中可以看到,如果 Future 的大小过大,Box::pin(future) 会从堆上分配内存,理论上会带来额外开销。这种设计可能基于几点考量:小型 Future 直接嵌入任务结构体中,能提升缓存命中率;而大型 Future 若嵌入,会让任务结构体过于臃肿,占用过多栈空间,反而不利于性能。

我通过实验验证,若 async 函数中包含较大的结构体,确实会导致 Future 执行变慢(即便计算逻辑相同):

RESULTS:--------Small Future (64B): 100000 iterations in 30.863125ms (avg: 308ns per iteration)Medium Future (1KB): 100000 iterations in 61.100916ms (avg: 611ns per iteration)Large Future (3KB): 100000 iterations in 105.185292ms (avg: 1.051µs per iteration)Very Large Future (10KB): 100000 iterations in 273.469167ms (avg: 2.734µs per iteration)Huge Large Future (100KB): 100000 iterations in 5.896455959s (avg: 58.964µs per iteration)PERFORMANCE RATIOS (compared to Small Future):-------------------------------------------Medium Future (1KB): 1.98x slowerLarge Future (3KB): 3.41x slowerVery Large Future (10KB): 8.88x slowerHuge Large Future (100KB): 191.44x slower

在微调这个 async 函数时,我发现了一些微妙的现象。为了让 data 跨越 await 存活,我特意在最后引用了它,以防编译器优化掉:

async fn huge_large_future() -> u64 {    let data = [1u8; 102400]; // 10KB * 10    let len = data.len();    future::yield_now().await;    (data[0] + data[len - 1]) as u64}

理论上,若改成下面这样,由于 len 在 await 前已计算完成,后面又没用引用到,生成的 Future 大小应该很小:

async fn huge_large_future() -> u64 {    let data = [1u8; 102400]; // 10KB * 10    let len = data.len();    future::yield_now().await;    0}fn main() {    let ex = Executor::new();    let task = ex.spawn(huge_large_future());    let res = future::block_on(ex.run(task));    eprintln!("Result: {}", res);}

然而,我发现 data 仍被保留在状态机中,即便 len 未被后续使用。这涉及到编译器如何判断变量是否跨越 await 存活的问题。当然,若显式限定 data 的生命周期在 await 之前,它就不会被纳入状态机:

async fn huge_large_future() -> u64 {    {        let data = [1u8; 102400]; // 10KB * 10        let len = data.len();    }    future::yield_now().await;    0}

编译器如何判断哪些变量应该保存

我查阅了 Rust 编译器的源码,发现变量是否跨越 await 存活由 locals_live_across_suspend_points 函数 决定:

/// The basic idea is as follows:/// - a local is live until we encounter a `StorageDead` statement. In///   case none exist, the local is considered to be always live./// - a local has to be stored if it is either directly used after the///   the suspend point, or if it is live and has been previously borrowed.

在我们的代码中,let len = data.len() 构成了对 data 的借用,因此 data 被保留在状态机中。或许这里仍有优化的空间?我去社区问问看。

结语

所有实验代码均可在以下链接找到:async-executor-examples

在 Rust 异步编程中,代码的细微调整可能引发性能的显著波动。深入理解状态机生成的内在机制,能助你打造更高效的异步代码。下次编写 async fn 时,不妨自问:这个状态机究竟有多大?

Fiber Network: 基于 CKB 实现的闪电网络

作者 yukang
2025年3月16日 16:53

最近一年我在做 Fiber Network 这个新的开源项目,上个月底刚好主网第一个版本发布

这个项目的挑战还是挺大的,上主网只是一个新的开始。我在开发过程中学到了很多东西,这是我前段时间写的一篇关于 Fiber 的大致介绍。

Fiber 简介

Fiber 是基于 CKB 构建的闪电网络协议,旨在实现快速、安全且高效的链下支付解决方案。借鉴了比特币闪电网络的核心理念,Fiber 针对 CKB 的独特架构进行了深度优化,提供低延迟、高吞吐量的支付通道,适用于微支付和高频交易等场景。与传统的闪电网络不同,Fiber 拥有多项关键特性:

  • 多资产支持:不再局限于单一币种,能够处理多种资产交易,为复杂的跨链金融应用铺平道路。
  • 可编程性:基于 CKB 的图灵完备智能合约,支持更复杂的条件执行和业务逻辑,拓展了支付通道的应用边界。
  • 跨链互操作性:原生设计支持与其他 UTXO 链(如比特币)的闪电网络交互,提升了链间资产流动性和网络兼容性。
  • 更灵活的状态管理:得益于 CKB 的 Cell 模型,Fiber 可以更高效地管理通道状态,降低链下交互的复杂度。

在这篇文章中,我们将从源码层面介绍 Fiber 的整体架构和主要模块,以及项目的后续展望和规划。

前提知识

  • Rust, and actor framework,Fiber 是一个完全由 Rust 编程语言所实现的项目,另外我们在实现中采用了 actor model 的模式,依赖社区的项目 ractor 框架。
  • Lightning network,Fiber 的基本思想沿用了 Bitcoin 的闪电网络,基本原理是一致的,所以 Mastering lightning network 和 Bolts: lightning/bolts 是非常有用的参考资料。
  • CKB transaction and contract,Fiber 会通过 RPC 与 CKB node 进行交互,比如 funding transaction 或者 shutdown commitment transaction 可能需要通过 RPC 提交给 CKB 的节点,所以掌握 Fiber 需要了解一些 CKB 合约开发方面的知识。

重要模块

我们从最高纬度去看一个 Fiber Node,主要包含下面几个主要模块:

其中:

  • Network Actor 是 Fiber Node 中负责节点内外的消息通信
  • Network graph 包含一个节点对于整个网络里其他节点和 channel 的信息,当一个 Fiber Node 收到一个支付请求的时候,我们首先会尝试从 network graph 中找到一条路径能够触达收款节点,这个 network graph 结构是跟着网络上的 gossip 信息不断更新的
  • PaymentSession 负责管理一个支付的生命周期
  • fiber-sphinx 是我们自己实现的 onion packet 加解密 Rust 库
  • Gossip 是 Fiber 节点之间的交换网络消息的协议,用于 Node 和 Channe 的发现和更新。
  • Watchtower,这里负责监听 Fiber node 所关心的 channel 里面的重要事件,另外如果某个 Node 提交一个老的 commitment transaction,watch tower 负责发出 revocation transaction 来进行惩罚
  • Cross hub,这个模块负责跨链的互操作,比如付款者通过 Bitcoin 的闪电网络发出 Bitcoin,而接收者收到的是 CKB,cross hub 这里会进行一个转换,将 Bitcoin 的 payment 和 invoice 和 Fiber 这边的 payment 和 invoice 进行映射管理
  • Fiber-script 在一个单独的代码仓库,这里面包含了两个主要的合约,funding-lock 是一个资金锁定合约,使用 ckb-auth 库来实现一个 2-of-2 多重签名,commitment-lock 实现了 daric 协议来作为 Fiber 的惩罚机制

Actor Model 和 Channel 管理

Channel 的管理是闪电网络中非常重要、也是异常复杂的部分。其中的复杂性主要来自于 Channel 内部数据和状态的改变来自于网络上 peer 之间的交互,事件的处理可能存在并发上的问题,一个 Channel 的双边可能同时都有 TLC 的操作。

闪电网络本质上是一个 P2P 系统,节点之间通过网络消息相互通信进而改变内部的数据状态,我们发现 Actor Model 非常适合这种场景:

Actor Model 极大地简化了代码实现的复杂度,使用 Actor model 后我们不需要使用锁来保护数据的更新,当一个 Message handle 结束的时候,我们会把 channel state 的数据更新写入 db。而像 rust lightning 如果没用使用 actor model,就可能会涉及到非常复杂的锁相关的操作

我们的所有的重要模块都采用了 Actor Model,Network Actor负责节点内外的消息通信,比如一个节点要给另外一个节点发送 Open channel 的消息,这个消息首先会通过 Fiber node A 的 channel actor 发送到 network actor,node A 的 network actor 通过更底层的网络层 tentacle 发送到 node B 的 network actor,然后 network actor 再发给 node B 里面的所对应的 channel actor。

在一个 Fiber Node 内部,每一个新的 Channel 我们都会建立一个对应的 ChannelActor,而这个 ChannelActorState 里面包含了这个 Channel 所需要持久化的所有的数据。采用 Actor Model 的另外一个好处就是我们能够在代码实现过程中直观地把 HTLC 网络协议相关的操作映射到一个函数里,比如下图中展示了 HTLC 在多个节点之间的流转过程,对于 A 到 B 之间的 AddTlc 操作,节点 A 里的 actor 0 所应对的代码实现就是 handle_add_tlc_command,而节点 B 里的 actor 1 所对应的代码实现是 handle_add_tlc_peer_message

Channel 之间的 TLC 操作是复杂度非常高的部分,我们在实现上延用了 rust-lightning 的方式,使用状态机来表示 TLC 的状态,根据 actor 之间的 commitment_sign 和 revoke_ack 的消息来改变状态机,总的来说 AddTlc 的操作流程和两个 Peer TLC 状态的改变过程如下:

支付和多跳路由

每个 Fiber 节点都通过 Network graph 保存了自己对于整个网络的了解情况,本质上这是一个双向有向图,每一个 Fiber 节点对应于 Graph 里面的一个 vertex,每一个 Channel 对应于 Graph 里面的一个 edge,出于隐私保护的需求,Channel 的真实 balance 不会广播到网络中,所有 edge 的大小是 Channel 的 capacity。

在支付开始前,发起者会通过路径规划找到一条通往收款者的路径,如果有多条路径就需要找到各方面综合考虑最优的路径,而在信息缺失的图中找到最优路径是一个在工程上非常具有挑战性的问题,Mastering Lightning Network 对这个问题有很详细的介绍

在 Fiber 中,支付动作由用户向 Fiber Node 通过 RPC 发起请求,节点收到请求后会创建对应的 PaymentSession 来追踪支付的生命周期。

目前我们的路径规划的算法是一个变形的 Dijkstra 算法,这个算法是通过 target 往 source 方向扩展的,搜索路径的过程中通过折算支付成功的概率、fee、TLC 的 lock time 这些因素到一个 weight 来进行排序。其中的概率估算来自于每次支付的结果记录和分析,实现在 eval_probability。路径的选择质量好坏对于整个网络的效率和支付的成功率非常重要,这部分我们今后将会继续改进,Multipart payments (MPP) 也是一个今后可能要实现的功能。

路径规划完成后下一步就是构建 Onion Packet,然后给通过 source node 发起 AddTlcCommand。后续如果 TLC 失败或者成功会通过事件通知的方式处理。

整个支付的过程可能会发生多次的重试,一个常见的场景就是我们使用 capacity 作为 Graph 里边的容量,可能路径规划出来的路线无法真实满足支付的大小,所以我们需要返回错误并更新 Graph,然后再继续自动发起下一次路径规划尝试进行支付

节点广播协议 Gossip

Fiber 的节点之间的通过相互发送广播消息交换新的 Node 和 Channel 信息,Fiber 中的 Gossip 模块实现了 Botls 7 定义的 routing gossip。在实现过程中我们的主要技术决策在这个 PR: Refactor gossip protocol里面有描述。

当一个 Node 节点第一次启动的时候,会通过配置文件里的 bootnode_addrs来的连接第一批 peers,广播消息的类型有三类:NodeAnnouncementChannelAnnouncementChannelUpdate

Fiber 会把收到的广播的原始数据保存下来,这样方便通过 timestamp + message_id 组合的 cursor 来对广播消息进行检索,以方便来自 peer node 的 query 请求。

当一个节点启动的时候,Graph 模块会通过 load_from_store来读取所有的 messages,重新构建自己的 network graph。

我们采用基于订阅的方式在网络中传播消息。一个节点需要主动向另一个节点发送广播消息过滤器(BroadcastMessagesFilter),另一个节点收到了该消息之后会为其创建对应的 PeerFilterActor,在构造函数里创建 Gossip 消息订阅。通过基于订阅的模型这种方式,我们可以让其他节点接收在特定的 cursor 之后接收到新保存的 Gossip 消息

隐私 Onion 加解密

处于隐私保护的需求,payment 的 TLC 在多个节点之间传播的时候,每个节点只能知道自己所需要的信息,比如当前节点接收的 TLC 的 amount、expiry、下一个传播的节点等信息,而无法获得其他不必要的信息,而且每个 hop 在发送 TLC 给下一个节点的时候也需要做相应的混淆。

类似的,如果 payment 在某个节点传播的过程中发生了错误,这个节点也可能返回一个错误信息,而这个错误信息会通过 payment 的 route 反向传递给 payment 的发起节点。这个错误信息也是需要 Onion 加密的,这样确保中间节点无法理解错误的具体内容,而只有发送者能够获得错误内容。

我们参考了 rust-lightning 在 onion packet 的实现,发现其实现方式还是不够通用 (会绑定于其项目的具体数据结构),所以我们自己从头开始实现了 fiber-sphinx,更详细的内容请参考项目的 spec。

涉及到 Onion 加解密的几个关键节点在这三个地方:

Watchtower

Watchtower 是闪电网络中的重要安全机制,主要用于帮助离线用户防止资金被盗。它通过实时监测链上交易,并在发现违规行为时执行惩罚交易,从而维护闪电网络的公平性和安全性。

Fiber 的 watchtower 实现在 WatchtowerActor里,这个 actor 会监听 Fiber 节点中发生的关键事件,比如一个新的 Channel 创建成功时将会收到 RemoteTxComplete,watchtower 就在数据库里插入一条对应的记录来开始监听这个通道,Channel 双方协商成功关闭时会收到 ChannelClosed,watchtower 从数据库中移除对应的记录。

在 Channel 中 TLC 交互时候,watchertower 将会收到 RemoteCommitmentSignedRevokeAndAckReceived,分别去更新数据库中存储的 revocation_datasettlement_data,这些字段将会在后续创建 revocation transaction 和 settlement trasaction 的时候用到。

Watchtower 的惩罚机制是通过比较 commitment_number 来判断 CKB 的链上交易是否使用了老的 commitment transaction,如果发现违规则构建一个 revocation transaction 提交到链上进行惩罚,否则就构建发送一个 settlement transaction 提交到链上。

其他技术决策

  • 存储:我们使用 RocksDB 作为存储层,写代码的过程中可以直接使用 serde 来序列化。但因为 scheme-less,所以不同版本的数据迁移仍然是一个挑战,我们通过这个独立程序来解决,比较粗暴,但目前没想到更好的办法。
  • 序列化:节点间的消息使用 Molecule 进行序列化和反序列化,带来效率、兼容性和安全性优势。要确保确定性,这样相同的消息在所有节点上序列化方式相同,这对于签名生成和验证非常重要。

后续展望

目前 Fiber 还处于前期活跃开发阶段,后续我们可能将继续做以下几个方面的改进:

  • 修复还未处理好的 corner case,增强项目整体的健壮性
  • 目前的 cross hub 还处于 Demo 阶段,我们会对这部分增加如 payment session 等功能
  • 完善支付路由规划算法,可能会引入其他路径搜索策略,以适应用户不同的路由偏好和需求
  • 扩展合约的功能,比如引入基于版本号的撤销机制和更安全的 Point Time-Locked Contracts

Let’s scale P2P finance together! 🩵

2024:简单的理想生活

作者 yukang
2025年1月1日 08:03

2024 年快结束了,在这最后的一两个小时里我写着这篇年终总结准备跨年了,顺着大致时间线来回顾一下就好了。

年初就起了个好头,众多加密货币开始上涨。总体而言,2024 年是个加密货币和区块链的大年。有那么一小段时间我每天都在关注涨跌,渐渐地我发现这个领域涨跌都是太频繁了,而过多关注除了浪费时间并没有什么大的用处。因为两年前开始在这个领域工作,所以我自然也会投资一些加密货币。刚开始我稍微接触了一下合约,但很快亏掉了几千元,算是交了学费。然后很快理智地退出了,合约本质上来说和赌博有点类似,钱来得也快亏得也快,但大概率是要亏钱的。

我听从了一些行业老鸟的建议,拿住比特币就行,其他的看着买点。我从 2023 年开始陆续买入了一些比特币,当时的价格不算高,到今年年底看来也有不少涨幅了。我抱着长期拿住的心态在买入,打算至少持有八九年以上。所以现在我基本不怎么关心价格了,如果买了就当作这钱是存在那里好了,把时间幅度拉长,我相信比特币未来会更值钱。我愿意相信这个行业是因为从技术的角度考虑是即有趣又有挑战。这两年来我工作的项目和比特币是非常类似的,就当作为信仰充值。

2024 年 5 月开始我投入到了公司的一个新项目开发上,这是个完全开源的项目叫作 nervosnetwork/fiber,简而言之就是 CKB 上的闪电网络实现。所以 2024 年的大部分时间我都专注于这个项目,因为这是个新项目所以很多功能都是从头开始实现,这对于程序员来说时段快乐时光,毕竟维护老项目很多时候都是在考虑兼容性,没有什么大量写代码的快感。

闪电网络似乎现在已经过了最火的时候,但却是古典区块链技术的代表。如何在去中心的环境中构建出信任通道,这是个非常复杂的问题,大多数时候我们都是在参考 BOLT这个规范。开发过程中一直需要考虑的是这样安全么,如果对方出错或者发出恶意的请求会怎样,channel 的基本保证是任何时候任意一方都可以退出,而不会造成资金上的损失,另外还需要兼顾的是隐私的问题,所以支付的多跳传输需要使用洋葱加密,错误的返回链路上也需要用洋葱加密。反正本质上,这些都归结为数学问题,多签、加密和解密、哈希时间锁合约,确保了交易的不可伪造性和隐私性。我不打算继续在这篇文中写更多关于闪电网络的技术细节,也许以后会写一系列的相关文章。

总体来说,2024 年又开心地写了一年代码,甚至我觉得技术越做越有意思了:

远程工作两年后,我更多采用把问题留在脑海中,时不时拿出来思考的工作方式。有几次这样的经历,我像是在睡觉的过程中还在思考某个问题,然后第二天起来还记得当时想出来的办法。

另一方面,有些遗憾的是我今年参与 Rust 等开源项目的时间比较少了,写文章也比较少。似乎在公司的项目上工作得足够有趣、找到了足够的收获感,没有多少动力和时间去做其他项目。但意想不到的是今年年底还是收到了 Rust 基金会的邮件,愿意资助我一年继续做贡献。所以明年我应该还是会把一些业余时间投入到 Rust 项目上,这也算是把爱好折腾成了责任和义务。可以说 Rust 延长了我的技术生命,让我幸运地投入到一堆 Rust 开源项目上,并且找到适合自己的公司,以远程的方式工作。

因为整天除了带娃和宅在家编程,2024 年我似乎没认识什么新的人,社交圈很小,甚至到了年底我才想起是不是该约上许久不见的朋友线下聊聊。我不知道如何解决这个问题,这有一半是远程工作带来的副作用,另一半就是人到中年在社交上的需求小了。我还在 Cambly 上练习口语,这已经变成了我强迫自己和人沟通的一个渠道,我每周三节课一共一个半小时,其中一个小时大多数都是和我的固定老师聊,他比我大 10 岁左右,我们聊过很多话题,我给他科普区块链等技术领域、做模拟演讲等。另外我喜欢找那些一直在旅游的人或者退休了的人聊,因为通常能听到一些好玩的事情,有次有个一直满世界漂流的人对我说他希望的是 die with my boots on,我一下子没听出其含义,后来通过他的解释我知道了这个俗语的意思:一个穿着靴子死去的人会一直生活和战斗到最后,他们像往常一样生活时去世,而不是因为年老和因疾病、体弱等卧床不起,对他来说他希望自己死在旅游的途中。我想这种生活态度真是太好了,而且他也在践行自己的这种生活方式。我喜欢看那些一直在路上的博主,比如 十三要和拳头刘伟元的旅行,可能正是因为我已经不太可能做到像他们那样随心所欲地玩耍。

说到旅行,今年五月底公司团建我们去了大理待了一周,那里的风景和气候都还挺不错,有些地方显得商业化太重,但沿着洱海骑行和在苍山徒步都非常惬意。夏天我和家人去了一趟北方,走的是比较热门的路线,青岛、威海、大连。不过这趟很累,因为暑假期间都是家长带着孩子,所以去哪里都是人挤人,但其实孩子们也还太小,他们只是想找个地方玩沙子赶海,而对于历史遗迹之类的地方则完全不感兴趣。

11 月公司组织去了趟清迈,我们在那里举行了第一次的 CKCON,我也是第一次用英语做技术演讲。感觉清迈的基础设施还有待提升,有一次我一个人打车,司机好像是中途拐进了城中小道上歪歪扭扭的乱窜,我开始担心自己会不会被拉去割腰子。其实司机是个好人,到了终点后我才发现自己的 Grab 不能付款,他就耐心得等我去找人借现金。

我很喜欢公司组织的线下聚会,不但可以和平时合作的同事见面聊聊,也可以暂时从一直带娃的生活中抽离出来,每次出去我的感受是这样的:

所以带孩子真的很累么?确实比较累,而且得看这个孩子是几岁。我喜欢带两三岁到五岁这个年龄段的孩子,因为这时候的孩子都是天真,又比较听话。像我大女儿到了七岁八岁,开始有自主意识了就很淘气,很多时候也不怎么听话,有时候会让我焦头烂额。小学二年级的作业比较多,我女儿每天需要在家里花大概一个小时来写作业,而且现在的数学作业看起来很多应用题,像我女儿这种没接受过幼小衔接的做起来就很慢,肯定需要家长帮忙。有时候孩子做了坏事,我会想起自己小的时候也做过类似的事情,但我现在已经变成了孩子眼中那个严格的父亲了。有次父亲看我对孩子发火,就对我说对孩子还是要适当宽容一些,然后提起小时候每次打了我之后都会心里很后悔,我听了就很感慨。

今年下半年开始,我又开始经常打篮球了。刚开始主要是为了缓解久坐的疲劳,后来就变成每天不断地提升自己的投篮技术。深圳的秋冬季节很舒服,我经常中午 11 点半去小区篮球场投篮差不多一个小时,顺便晒晒太阳。每天这样练习之后投篮技术有了很大的提升,无人防守的情况下基本有 70% 左右的命中率。一个人投篮这种事情看起来很枯燥和无聊,但其实沉下心来运动的感受非常好,我把刻意练习的心态投入到了这个项目上,那一个小时内能达到类似心流的状态,时间变得清澈,仿佛只有我和篮球了。投篮最重要的是掌握出手时候的平衡度,手腕和手指用力,让篮球后旋起来,练习多了投篮动作就形成了肌肉记忆,只要动作做完就大致能知道是否命中,篮球空心入网的声音真是太悦耳了。磨练技艺真是一种最好的状态,而编程、写作、篮球都是这样的事情。

打篮球已经是我整整 20 年的爱好了,但我从未好好练习过投篮,可惜左膝盖在 2017 年伤过一次,运动激烈了容易酸疼,所以再也不怎么去和年轻人打半场了,即使偶尔玩玩总是担心自己受伤,在场上变得畏手畏脚。那些之前理所当然的事情变得奢求了,能力和自由渐渐地丧失,这真是大龄带来的切身痛苦。

有一次我傍晚还在练习投篮,有个看起来比我大七八岁的大哥过来,渐渐地我们聊了起来。我看他的篮球鞋很漂亮,他说是他儿子的,应该叫作空军一号。我们边投篮边聊天,一直聊到天完全黑掉看不到篮筐。没想到这样一个在国企工作的大哥也经常翻墙看新闻,说这几年的情形是聪明人都在蛰伏和休息。还有一次我正在投篮,刚好碰到一个幼儿园班的小朋友们经过,因为球场上就只有我一个人在锻炼,他们就围在场边观看,渐渐地我每进一个球小朋友们就开始欢呼,每次没进就惋惜叹声,这真是个有趣的经历。日子大多平淡如水,但这些小瞬间却留在了心里。

回想起来,今年生活中的一些其他变化,彻底不看朋友圈,不怎么追新闻,总体来说信息更闭塞了。但 2024 却是我生活上最朴素充实的一年,上班做感兴趣的项目下班做喜欢的运动,在我做了很多减法后,现在的生活好像就是自己理想中的状态。

祝各位新年快乐!

CKB new script verification with VM pause

作者 yukang
2024年11月7日 20:03

CKB 相关技术文章第三篇。

背景

CKB 的每一个交易在提交到交易池之前都会经过一个 script verification 的过程,本质上就是通过 CKB-VM 把交易里的 script 跑一遍,如果失败了则直接 reject,如果通过了才会继续后面的流程。

这里的 script 就是一种可以在链上执行的二进制可执行文件,也可以称之为 CKB 上的合约。它是图灵完备的,我们通常可以通过 C、Rust 来实现这些 script,比如 nervosnetwork/ckb-system-scripts 就是 CKB 上的一些常用的系统合约。用户在发起交易的时候就设置好相关的 script,比如 lock script 是用来作为资产才所有权的鉴定,而 type script 通常用来定义 cell 转换的条件,比如发行一个 User Define Token 就需要指定好 UDT 所对应的 type script。script 是通过 RISC-V 指令集的虚拟机上运行的,更多内容可以参考 Intro to Script | Nervos CKB

大 cycle 交易的挑战

通常一个简单的 script 在 CKB-VM 里面执行是非常快的,VM 上跑完之后会返回一个 cycle 数目,这个 cycle 数量很重要,我们用来衡量 script 校验所耗费的计算量。一个合约的 cycle 数多少,理论上来说依赖于 VM 跑的使用用了多少个指令,这由 VM 在跑的时候去计算 VM Cycle Limits

随着业务的复杂,逐渐出现了一些大 cycles 的交易,跑这些交易可能会耗费更多的时间,但我们总不可能让 VM 一直占着 CPU,比如在处理新 block 的时候,CPU 应该在让渡出来。但之前 CKB-VM 对这块的支持不够,为了达到变相的暂停,处理大 cycles 的时候我们可以设置一个 step cycles,假设我们设置为 100 cycles,每次启动的时候就把 max_cycles 设置为 100,这样 VM 在跑完 100 cycle 的时候会退出,返回的结果是 cycle limitation exceed,然后我们就知道这个 script 其实是没跑完的,先把状态保存为 suspend,然后切换到其他业务上做完处理之后再继续来跑。回来后如何才能恢复到之前的执行状态呢,这就需要保存 VM 的 snapshot,相当于给 VM 当前状态打了一个快照:


根据这个机制,我们老的 script 校验大交易的整个流程是通过一个 FIFO 的队列保存大交易,然后通过一个后台任务不断地从这个队列中取交易跑 VM,每次都跑 1000w cycle 左右,在这个过程中就可能切换出去,没跑完的交易继续放入队列等待下一次执行:

对应到代码就是 ChunkProcess 这个单独服务来处理的。由于 ChunkProcess 是一个单独的服务,它的处理流程和其他交易的处理流程是不一样的,这样会导致代码的复杂度增加,比如:

  1. 要针对 ChunkProcess 里面的交易额外判断,例子 1, 例子 2
  2. 暂停 / 恢复 ChunkProcess 处理的时候,需要对 ckb-vm 做相关的状态保存和恢复处理,参考结构 TransactionSnapshot, 代码比较复杂且容易遗漏,历史上也有过相关的 bug 1, bug 2, 以及安全问题。
  3. 代码中包含重复逻辑,比如 chunk_process 里的 process_inner_resumeble_process_tx
  4. 由于它只能同时处理一个大 cycle 交易,在 tx pool 本身比较空闲的情况下如果收到了多个大 cycle 交易也不能并行处理,比如 .bit 团队之前有过反馈他们通过本地 rpc 同时提交多个大 cycle 交易会比较慢的问题。

CKV-VM pause

这些问题的根本是 VM 只能通过 cycle step 的方式来暂停,有没有一种方式是我们任何时候想暂停就暂停,就是 event based 的方式。所以后来 CKB-VM 团队做了一些改进:

这个方法的本质是通过 VM 的 set_pause 接口,把一个 Arc<AtomicU8> 的 pause 共享变量设置给 VM。然后在 VM 外通过更新这个 pause 的变量让 VM 进入暂停状态或者继续执行,这样我们就不需要 dump snapshot 等操作,因为 VM 整个就还是在内存中等着:

新的实现方案

基于这些改进我们可以重新设计和实现 CKB verify 这部分的代码,主要是为了简化这部分代码,并且提高大交易处理的效率。这是一个典型的 queue based multiple worker 方案:

主要的核心是就是这段异步执行 VM 的逻辑:chunk_run_with_signal。做的过程中发现一些其他问题:

  • 交易提交的时候,SubmitLocalTxSubmitRemoteTx 如果 verify 失败目前会立即返回 Reject,如果改成加入队列的方式,这个结果无法实时给到,所以做了如下改动:
    • 优先处理本地的交易,本地提交的交易不会放入 queue,而是直接会在 RPC 的处理阶段执行
    • 所有的来自网络 peer 的交易都全到放入到 queue
  • 后来 CKB vm 又新增了 spawn 的实现,所以会有 parent、child 的概念,那么Child VM 是执行 syscall 的时候执行 machine.run ,如果不改这块执行 child vm 的时候不可暂停
    • 后来我们讨论了之后决定 spawn 时把父的 Pause 传递给子,然后暂停的时候给父的 Pause 设置暂停,这样所有的子 machine 同样返回 VMError::Pause ,同时把当前的 machine 栈重新入栈,恢复的时候继续执行,这里逻辑比较重,相关代码实现:run_vms_child
  • 后来用重新设计了 spawn,使用了一种新的 determined scheduler 的方式去管理所有的 vms 和 IO,之前和 VM 的使用者角度来说之前需要和 VM 交互,现在变成了都通过 scheduler 来管理。关于 spawn 的设计参考这个文档:Update spawn syscalls

整个 PR 在这里:New script verify with ckb-vm pause

CKB RBF 设计和实现

作者 yukang
2024年11月6日 19:55

CKB 相关技术文章第二篇。

Replace by fee

问题

如果一个交易成功发送到交易池,但可能出现因为费用较低而一直得不到处理。之前 CKB 没有其他措施来处理这种情况。

例如 Dotbit 4 位域名注册拥堵 这个事故发生过程中,CKB 的应用方无法使用任何方式来尽快让自己的交易被打包,这就是引入 Replace-by-fee(RBF) 的原因,我们需要一个机制来提高已经在交易池里交易的费用,替换掉旧的交易,让新的交易尽快被打包。

在新的 multi_index_map 重构后,交易在 pending 阶段也会按照交易的 score 来优先处理 (通常费用高的交易 score 也会高),这会避免高费用的交易被阻塞住,所以理论上述需要手动提高费用的情况会减少,但我们还是需要 RBF 来手动提高交易的费用,应对意外的情况。

另外,RBF 可能将多个老的交易替换出去,因此也是将两个或多个支付合并为一的方法,例如下图所示,如果满足条件 tx-a, tx-b, tx-c, tx-d 都会被 tx-e 这个交易替换掉:

参考

中本聪最初的 Bitcoin 版本中就有引入一个 nSequence 的字段,如果相同交易的 nSequence 更高,就可以替换之前老的交易,这个实现的问题是没有支付额外的 fee,miner 没用动力去替换交易,另外因为没有 rate-limiting 从而导致可能被滥用,所以 Bitcoin 在 0.3.12 版本中禁止了这个功能。后来 Bitcoin 重新引入了新的 RBF 改进,主要包括需要支付额外的费用来替换老交易,另外为 RBF 指定了更多的限制条件。

在 CKB 上我们之前做过两次 RBF 的相关调研,因为之前 Pending 是一个 FIFO 的数据结构,所以处理替换不是很方便,在 RBF in CKB(draft 2023.01.05) 尝试引入一个 high priority queue 来实现 inject-replace。交易池改造之后,整个交易池可当作一个优先队列,所以应对 RBF 会简单很多。

新增 RBF 的流程

实现细节

  • pre-check 为 entry 加入到 tx-pool 之前必须要做的检查,之前只是做双花的检查,新增 RBF 后如果双花检查失败(这里意味着冲突),继续做 RBF 的相关检查,如果 RBF 检查成功则也返回成功,否则直接返回错误。这里默认直接做 resolve_tx 的检查,如果成功则走正常流程,目的是不给正常流程增加额外成本。所以这就是pre-check 修改后的主要逻辑

RBF 的检查规则参考 Bitcoin 的六条,check_rbf 初步实现

实现细节:(Bitcoin Core 0.12.0)~~1. 交易需要声明为可替换交易~~ 2. 新替换交易没有包含新的、未确认的 inputs3. 新替换交易的交易费用比待替换交易费用高4. 新替换交易费用必须比节点的 min relay fee 高5. 待替换交易的子交易数量不可超过 100 条(即使用了该交易的任意 outputs,该交易替换后它们将被从内存池中移出)6. 因为 ckb 是做了两步提交,我们新增规则:被替换的交易只能是 Pending 或者 Gap 阶段的。

我们不给交易加新的字段表示是否可以被替换,而是通过节点是否配置了 min_rbf_rate 来决定是否能做替换,因此 规则 1 不做对应考虑。

替换和提交

修改 tx-poolsubmit_entry 函数,传入 conflicts,在新增 entry 之前把所有冲突的交易删除 放入 rejected 记录,另外确保所有检查完成了之后才做删除和写操作:submit_entry 逻辑

最终实现在这个 PR 里Tx pool Replace-by-fee

并发的 Bug

在最初的实现版本中,隐藏了一个并发的 bug 后来在测试发现了。RBF 的检查如果放在 pre-check 中,如果多个线程中的多个交易发生了冲突,input resolve 可能会出问题。Fix concurrency issue for RBF 这个 PR 修复了这个问题,把 RBF 的冲突检查移动了 submit entry 之前,因为在这个函数里面会持有 write 锁。

cycling attack

后来我们在做闪电网络的时候又发现 RBF 可能会引入 cycling attack 的风险,这个攻击通过构造巧妙的新交易,让支付路径上的中间节点的 commitment tx 不能按时上链,Lightning Replacement Cycling Attack Explained这篇文章有更详细的描述。

所以我们后来又做了这么一个改进:Recover possible transaction in conflicted cache when RBF 来规避这个问题。

CKB 交易池重构

作者 yukang
2024年11月6日 19:39

在 11.9 号清迈的 CKCON 会议上我会做一个关于 CKB 交易池的演讲,这是我准备的 slides Key Upgrades of the CKB Core 。所以这段时间在整理之前做项目的时候写的一些文档,顺便分享到自己的博客上。既然我们整个项目的源码都是公开的,这些文档其实也是可以分享的。

第一次听说 CKB 的读者可以参考这个文档以了解什么是 CKB 以及如何工作的:How CKB Works | Nervos CKB

我加入 Cryptape 之后一年内做的主要工作,涉及到交易池重构、Replace-by-fee 功能、以及 new-verify。这是第一篇关于交易池重构的文章。

什么是交易池

在 bitcoin 中交易池叫作 mempool,比如 mempool - Bitcoin Explorer 这个网站就很好地展示了其当前的状态。

交易池是 bitcoin 中的一个重要的组件,但感觉专门关于这块的资料很少,只能通过 PR 和邮件列表上的讨论看到一些文档。但交易池非常重要,因为一个交易要上链必须会通过交易池,而其中的交易打包算法涉及到如何选择合适的交易,这里面有很多因素需要考虑,所以在实现上也是比较复杂的。

当一个交易被提交到一个节点时,或者一个节点从网络中同步到交易时,这个交易首先需要被加入到交易池中,交易池里会根据一定的算法去选择下一个需要被打包的交易,另外交易池作为一个缓存,我们需要为其设置一个最大的 size。所以交易池里面最重要的两个操作就是 packaging 和 evicting。

交易池里面的交易存在父子关系,打包的时候需要从交易链的纬度去考虑,后面的 Replace by fee 这些功能也需要关注整个交易的所有子交易。

交易池重构

问题

根据 RFC consensus-protocol 的设计,CKB 里的 tx-pool 采用了两段提交的方式:

相应地在交易池最初实现的时候, ckb 的代码实现中 tx-pool 同样采用了三个独立的队列,具体定义如下:

  • pending 交易刚加入到交易池时候的状态,我们每次只能处理不多于 MAX_BLOCK_PROPOSALS_LIMIT 个交易,交易需要先进入 gap 备选,具体代码逻辑在 update_proposals
  • gap 已经被 proposed 了,但是还不能被打包,需要等一个块后才能被打包,所以这只是内部中间过渡状态。
  • proposed 交易可以加入到 block_template.transactions , 最终打包到 block 里,具体代码逻辑在 block_assembler

实现中 pendinggap 同样都是使用了 PendingQueue(LinkedHashMap),而 proposed 采用了 SortedTxMap(HashMap + BTreeSet)

pub struct TxPool {    pub(crate) config: TxPoolConfig,    /// The short id that has not been proposed    pub(crate) pending: PendingQueue,    /// The proposal gap    pub(crate) gap: PendingQueue,    /// Tx pool that finely for commit    pub(crate) proposed: ProposedPool,    ....    pub(crate) expiry: u64,}

这样的实现存在以下问题:

  • 我们不容易对所有在交易池中的 entry 做统一排序,这样会存在以下问题:

    • 一个 fee 高的交易可能在 transaction 多的情况下在 pending 阶段一直卡着,因为我们在 pending 和 gap 阶段只是按照时间顺序来处理,只在 proposed 后的打包阶段按照交易费来处理。
    • issue 3942 交易池满了之后,我们需要选择一些 entry 做 evict,我们目前的 evict 逻辑很简单粗暴。我们希望尽量选择最小 descendants 的交易,这样能避免在 evict 过程中删除过多交易。我们目前在 pending 和 gap 阶段没有记录 descendants,而需要加入这个逻辑就和 proposed 阶段完全重复,而且因为不会统一排序,后续实现也不容易。
  • pending, gap 和 proposed 除了所采用的数据结构不同外,有很多逻辑雷同的代码,比如 entry 的新增和删除等操作,同样都维护了 deps 和 header_deps,resolve_conflict, resolve_conflict_header_dep, resolve_tx 等函数的逻辑也是类似的,但实现上有些细微差异,这导致长期来说代码不容易维护。

  • 同样我们在 tx-pool 上对 entry 做迭代和查询时,需要依次针对 pending, gap, proposed 做相同的逻辑,比如 resolve_conflict_header_dep 这样的函数在 pool 中有几个类似的,甚至 get_tx_with_cycles 这样的函数,需要依次判断各个队列。
  • 实现其他功能不方便,比如我们如果要实现 Replace by fee,就需要找交易池中和新交易有冲突的交易,我们需要在三个数据结构上分别进行检查才能得到结果。

方案

基于以上解决现有问题、应对未来的潜在需求、保持代码可维护性的角度,同时参考 Bitcoin txmempool 的实现,我们提出引入 Multi_index_map 对 tx-pool 进行重构。

总体方向是把所有的 entry 放入统一的数据结构中进行管理,加入一个新的字段 status 标识目前 entry 所处的阶段,然后通过 index_map 的方式根据不同的属性进行排序和迭代:

pub enum Status {    Pending,    Gap,    Proposed,}#[derive(MultiIndexMap, Clone)]pub struct PoolEntry {    #[multi_index(hashed_unique)]    pub id: ProposalShortId,    #[multi_index(ordered_non_unique)]    pub score: AncestorsScoreSortKey,    #[multi_index(ordered_non_unique)]    pub status: Status,    #[multi_index(ordered_non_unique)]    pub evict_key: EvictKey,    // other sort key    pub inner: TxEntry,}

其中根据 Rust 社区的 multi_index_map 内部实现采用的数据结构看,性能上应该没有什么大问题:

  • Hashed index retrievals are constant-time. (FxHashMap + Slab).
  • Sorted indexes retrievals are logarithmic-time. (BTreeMap + Slab).
  • Non-Unique Indexes
    • Hashed index retrievals are still constant-time with the total number of elements, but linear-time with the number of matching elements. (FxHashMap + (Slab * num_matches)).
    • Sorted indexes retrievals are still logarithmic-time with total number of elements, but linear-time with the number of matching elements. (BTreeMap + (Slab * num_matches)).
    • Iteration within an equal range of a non-unique index is fast, as the matching elements are stored contiguously in memory. Otherwise iteration is the same as unique indexes.

具体实现时我们是否把 inner 也放在 Slab 里面以后可以通过 benchmark 来选择,从实现的简洁性角度考虑统一放在一个数据结构里面更容易。

目前的实现版本:Tx pool rewrite with multi_index_map #3993

实现结果

我们首先只是做模块内的重构 (保持对外逻辑和以前一样),当然考虑引入了新的数据结构,不管是从性能上还是内存占用上都会有一些影响。

为了做统一排序这件额外的事,本质上我们引入了额外的 Map(FxHashMap 或 BTreeMap) 来存储,所以比以前需要更多内存。另外,我们有时候需要调用 get_by_status 来筛选某个状态的 entries,这在新的实现里面需要先从 index 里面找出 slab 的 id,然后再找到对应的 entry,所以必然也会比以前慢。

从最终的性能对比结果上,除了内存会稍微有增加,性能上没有大的变化。另外我们在实现的过程中对所用到的 Rust 包 multi-index-map 做了一些贡献:Non-unique index support, capacity operations, performance improvement & more by wyjin

Cryptape 招聘 - 区块链开发工程师

作者 yukang
2024年4月20日 19:42

公司最近出来一个招聘,主要是想招一个 C、Rust 的人,另外要求编程能力、英文读写,如果有 Linux 底层或者编译器的经验就更好了,不强求区块链背景:

HR 说这是 ckb-vm: CKB’s vm 项目的职位,简单来说这是一个基于 RISC-V 的虚拟机,这也是一个远程的职位。

想要尝试的欢迎联系我,邮箱:moorekang@gmail.com

xz-backdoor 观感

作者 yukang
2024年4月5日 02:04

写写最近一周的大瓜 xz-backdoor,该事件可能成为开源供应链安全的一个分水岭,从技术角度看,这里面的社工和混淆也是精彩。

简单介绍一下背景,xz 是一个开源的无损压缩工具,在出事之前可能很少有人注意到这个压缩库使用如此之广,几乎任何一个 Unix-Like 的操作系统里面都有 xz-utils。在两年多的时间里,一个名为 Jia Tan 的程序员勤奋而高效地给 xz 项目做贡献,最终获得了该项目的直接提交权和维护权。之后他在 libzma 中加入了一个非常隐蔽的后门,该后门可以让攻击者在 SSH 会话开始时发送隐藏命令,使攻击者能够跳过鉴权远程执行命令。

Timeline of the xz open source attack 总结了该事件的主要时间点,这里我挑一些关键节点:

潜伏

  • 2005 ~ 2008 xz 项目的初始版本,这是一个文件压缩算法,主要由 Lasse Collin 开发和维护。
  • 2021-10-29 ~ 2022-06-29 Jia Tan 开始较为密集地给 xz 项目贡献代码,同时几个类似马甲的账号 (Jugar Kumar, Dennis Ens) 在邮件列表里抱怨 Merge 得不到及时处理,问题得不到回复,有点逼宫的意思,在这个过程中项目主导者 Lasse Collin 把最近的优秀贡献者加入了维护者列表。

    准备

  • 2022-09-27 Jia Tan 获得了信任,并开始主导新版本的发布,他在这期间做了几个看似合理的 PR,但其实是在为今后的后门做伏笔,另一个马甲 Hans Jansen 提供了一个钩子可以让后门里的代码替换全局函数,从而绕过检查。
  • 2023-07-07 Jia Tan 在 Google 的 oss-fuzz 提供修改禁用了 ifunc,这也是为了避免 fuzz 可能发现后门。

    发动

  • 2024-02-23 Jia Tan 发布了第一个有害的 PR,在测试代码中包含了几个 binary 文件,这些文件看起来只用于测试,所以在代码 review 的过程中肯定不会被仔细查看。
  • 2024-02-26 Jia Tan 通过一个非常隐蔽的提交,给 CMakeList.txt 增加了一个 .,使得代码会编译失败从而让 Landlock 不会被激活。
  • 2024-02-24 Jia Tan 发布 v5.6.0,其中使用脚本混淆悄悄地把后门的 payload 塞进了目标文件中。Gentoo 和 Debian 开始在 unstable 版本中含有后门。
  • Hans Jansen 同时在发邮件催促 Debian 升级 xz 到 v5.6.1

    暴露

  • 2024-03-29: 一个叫 Andres Freund 的开发者在分析一个 sshd 可疑的 500ms 延迟时,发现了隐藏在 xz 的恶意后门。如果不是偶然的发现,估计现在世界上无数的服务器处于肉鸡状态,这位微软的员工如英雄一般拯救了世界。

攻击者是中国人?

从主要攻击者的名称看似乎是中国人,但 Git 昵称和时区这种东西很容易伪造,有人分析过开发者的代码提交时间,分析得出实际可能是欧洲人/以色列人冒充。

但不可否认,肯定会有不少国外的开发者会默认这就是中国人所为,我也看到了一些开发者开始带节奏,开始找各种和 Jia Tan 有过互动的中国程序员。

我倾向于相信这不是中国攻击者,感觉其 commit 信息里面的英文中没找到中式表达。比较确定的是,从这些马甲之间的密切配合来看,这像是一个有密谋的组织团体。

开源软件的脆弱性

开源意味着透明,但并不意味着安全。

10 多年前我们经历了 OpenSSL 的心脏滴血,如今类似的事情再次发生。甚至这次事件的性质更严重,心脏滴血漏洞本身是因为代码的逻辑问题导致被恶意利用,而这次是攻击者通过供应链恶意植入后门。

有一种观点是开源软件被更多人 review,所以理论上来说安全漏洞更容易被发现。但实际上看来,被巧妙设计过的代码改动,很不容易被发现问题,比如这次事件中这个提交,我相信绝大部分开发者无法发现被恶意添加的 .:

这次后门被发现有很大的运气成分,多亏了 Andres Freund 的细心和刨根问底的精神,这也算是有足够多的眼睛盯着所以发现了问题吧。

如何预防

如果有一个开源贡献者的身份识别机制,就可能预防类似的事情。我看到有人举例 Linux Kernel 提交必须使用 Git 的 Sign-off,但这个 Sign-off 更多的是在解决法律上的问题,Sign-off 本来就是因为法律诉讼而引入的。而且,在最坏情况下,一个开发者可能被社工或者入侵而导致身份被冒用,所以 Sign-off 并不意味着身份识别。

有的人提到通过支付来进行 KYC(Know Your Customer),这必然是不可能的,因为开源本来就是一个黑客文化的产物,大量的开发者会刻意选择使用匿名身份提交代码。

我们来看看 Bitcoin,如果论项目值钱程度,比特币的代码应该能排得上号。但比特币是支持 Permissionless and Pseudonymous development 的,甚至这是保证比特币去中心化的两个很重要的手段,中本聪的身份仍然是一个迷。中本聪选择匿名对比特币本身来说也至关重要,No one controls Bitcoin 是其价值根本。

那比特币如何保证不会被植入后门,比如这种供应链攻击?

  • Reproducible builds,这是个极大地缓解供应链风险的办法,不同的人编译相同的源代码必然得到相同的二进制文件,binary file 不能存在于源码库中。Bitcoin 使用 Guix container 从源码编译所有的东西,contrib: Enable building in Guix containers,这个过程可以在任何 Linux 发行版上重现。在这个过程中,几乎所有的一切都从源码编译,所以会存在一个鸡生蛋蛋生鸡的问题,为了解决这个问题必然会需要一些 binary files,但最好是将这个范围限制到最小,Preparing to Use the Bootstrap Binaries
  • Don’t forget to verify yourself!

另外比特币的安全在于 PoW,其设计本来就假设了少部分节点可能是恶意节点,除非黑客控制住了大部分节点才能造成破坏,而要达成这点在的概率可以认为就是零

开源的可持续性

从这个安全事件我们可以继续探讨开源的可持续性这个问题。这个事件中 xz 的维护者 Lesse Collin 看起来已经是处于疲于应付的地步。从贡献者统计可以看到这么多年几乎就是他一个人在给项目提交代码,Jia Tan 通过两年的潜伏就成为了贡献者第二的开发者:

长时间维护一个被大量使用的开源项目是个巨大的负担,对维护者而言不仅仅是时间的投入,有时候也是精神上的折磨,即使开发者当初的有多好的愿景,但谁也无法保证常年的持续投入。关于这点可以阅读这篇文章,The Dark Side of Open Source

Lesse Collin 在这次事件中被利用了这个弱点,他在这封邮件里解释到自己作为项目主导者的困境:

写到这里我想起自己也曾经催过一个库的作者,是不是考虑让更多人来维护项目 Maintenance status · Issue 😅。

也许未来可能有一套机制,能够让基础开源软件的维护者得到经济激励,但这条路如何演化出来我还没看出来,如果真的出来或许与加密货币有一定关联。

可怕的是,现在还有很多人没有意识到开源贡献者困境,那些价值几千上万亿的公司也是在期望开源的开发者能够像雇员似的响应他们的 High Priority:

这个世界上还是有无数的默默耕耘的开源代码维护者,比如 SQLite,全球大概有上万亿的 SQLite 数据实例跑在服务器上、手机上、浏览器里,但这个软件其实只由 3 个程序员维护了 20 多年;几乎所有工程师都使用的工具 curl,由 Daniel Stenberg 从 1998 维护到至今;vim 的作者 Bram Moolenaar 从 1991 年维护项目到自己去世,总共整整 32 年。

实际上没有人知道,多少被广泛使用的基础组件和代码是由各种默默无闻、分毫未取的开发者在用自己的业余时间维护着。

从这个角度看,人类数字基础设施这艘巨轮其实建立在非常脆弱的基础上,说不定哪天一个地方就裂开了。我现在养成了一个习惯,升级从来不追新,任何安装到自己电脑上的二进制都小心翼翼。

这个世界上有无数的恶魔,也会有一些英雄和吹哨人,致敬 Andres Freund。

从明天起,做一个 Rust 程序员

作者 yukang
2024年3月19日 00:28

3 月是怀念海子的月份:

从明天起,做一个 Rust 程序员,喂马、劈柴,周游世界。

10 年前我开始写第一行 Rust 程序,到如今全职远程做 Rust 开源项目,也许我真能去过喂马劈柴周游世界了😆。但回想自己的学习旅程,其中有各种曲折有几度放弃的时候,如果你也想学习或者提高 Rust 方面的技能,我这篇文章里有一条更容易的路。

为什么学习 Rust

Rust 1.0 发布已经快 10 年,所以并不是一门新编程语言了,从发展的角度来看 Rust 已经度过了生存期,并进入了迅速发展的阶段。从目前可见的业界方向来说,Rust 主要在以下几个方面取得了成功:

  • 在基础软件领域成为有力竞争者
    • 大量开源的 Rust 命令行工具和开发库,如果你使用 Python,可以通过 PyO3 用 Rust 来写对性能要求更高的模块,还出现了 opendal 这样优秀的基础库
    • Cloudflare 使用 Rust 开发新的网关 Pingora
    • 开源数据库实现,比如 QdrantRisingWavedatabend
    • AI 方面参考 Are we learning yet,虽然 Rust ML 生态系统还很年轻并处于试验阶段,但已经出现了一些雄心勃勃的项目和模块,Hugging Face 开源了 candle机器学习框架
  • 前端的基础设施
  • 操作系统
    • Windows 开始使用 Rust 开发一些核心组件
    • Rust 开始进入 Linux 内核,使得使用 Rust 开发 Linux module 成为可能
    • Andriod 使用 Rust 开发更多组件,并有效减少了内存方面的漏洞,他们发布的 Comprehensive Rust是一个很好的学习资料。Google 开始尝到 Rust 的好处,并开始投入更多资金和人力,近期 Google 打算捐献 100 万美金给 Rust 基金会着重解决 Rust 和 C++ 的互操作性
  • 区块链领域
    • 以我在这个领域工作一件多的经验来说,Rust 成为了区块链领域的标配,基本区块链相关的工作岗位 Rust 技能是一个极大的加分项
    • 大量公链使用 Rust 来开发
  • 游戏开发,参考 Are we game yet?,目前已经有成熟的游戏开发框架 Bevy Engine

如果你对 Rust 的发展情况感兴趣,可以参考 2023 Annual Rust Survey Results。在内卷的 IT 市场,作为程序员选择一门小众的编程语言是避免过度竞争的方式,我之前介绍过其他人的类似经验,我们称之为 The Niche Programmer。Rust 还未成为主流编程语言,但潜力和发展空间很大,而门槛相对其他语言比较高,所以我认为从求职的角度来考虑是值得一试的。

之前提到 Google 投入更多的资金在 Rust 上面,钱进来后相关的职位就出来了 C++/Rust Interop Initiative Software Engineer Lead

我学习 Rust 的体会

我 2014 年时践行每年学习一门新的编程语言,Rust 作为一门新的编程语言进入了我的视野。我开始使用 Rust 写些简单的个人学习项目,然后我继续做了 Rust exercises

后续几年我偶尔看看 Rust 相关的新闻和项目,时不时动手写点代码都会有点磕磕碰碰。直到四年前开始在 Github 上给一些 Rust 开源项目贡献,两年前开始给 Rust 编译器做贡献,一年前开始全职从事 Rust 区块链相关的工作。

从技术角度来说,Rust 非常有趣,这里面包含了近些年程序设计方面的一些良好实践。全职写 Rust 程序这一年多是我开发体验最好的阶段,当然有时候我们需要和编译器斗智斗勇、做类型体操,但很多问题在开发阶段给规避掉了。

Rust 的最大问题还是在于学习门槛相对较高,因为在 Rust 中程序员接触最多的 = 语义都变了。从我个人体验来说,在学会了 Rust 语法后会陷入一个瓶颈,如果日常工作中不使用 Rust,就没有多少机会去实践,另外不知道做一些什么项目。

我相信很多人同样如此,看了官方 tutorial 之后不知道如何下手,我想如果有一个经验丰富的老师带,会少走很多弯路,这就是我要介绍的极客时间训练营要解决的问题。

极客时间 Rust 训练营

说起来我与这个训练营还有些渊源。

当极客时间在筹划这个 Rust 训练营的时候,策划人员找到过我问我是否有意愿当这个课程的讲师。我还稍微犹豫了一下,因为我之前也想过如何在 Rust 领域做更多的分享,我很羡慕优秀的技术分享者比如 Jon Gjengset能够非常自如地通过视频分享 Rust 方面的技术。当老师当然是个机会能从沟通和表达方面提高这方面的能力。

后来考虑到自己时间方面安排不过来,我有全职工作、有业余的 Rust 社区工作、还有三个小孩,所以我应该真没时间去录制课程了,而且他们已经找到了我认为最合适的讲师:

我看了这个项目的大纲,陈天老师希望可以教大家怎么用 Rust 比较简单的语法和技巧,来完成 80% 的日常工作,主要是通过各种实践项目来学习,这也是我最推崇的 Learn by doing 的方式。

有很多主题我都没怎么接触过,比如构建一个 ChatGPT 的应用、比如跨平台 GUI 之类的,所以我对这个课程很感兴趣,然后我和策划说能不能做这个项目的助教,后来沟通下来发现当助教也需要不少时间的,所以就没机会参与到具体的教学里面了。

总之,这个项目对于想学习 Rust 或者已经有一定 Rust 经验,但想获得更多实践经历的人是非常合适的。在和极客时间的相关人员沟通的过程中,我发现他们做事情很用心,这个训练营的课程质量我认为是有保证的。

这个训练营一共是 15 周的课程安排,其中每周都会有明确的项目安排,课后还有助教答疑。关于训练营的更多信息请参考:极客时间训练营-Rust 训练营

我与陈天老师的小故事

我最早知道陈天是他写的公众号《程序人生》,他是那种技术和文笔都非常棒的程序员,非常难得。我还看过他的 B 站上的技术讲解视频,他的演讲和分享都很流畅。陈天是极客时间《陈天 · Rust 编程第一课》专栏作者,已有 2.3w 人学过,广受好评。技术能力、演讲表达、对技术的热情这些都是讲师最重要的素质要求,所以陈天是这个训练营最好的讲师人选。

再分享一个小故事,我一年多前跳槽的时候还有些犹豫,因为自己的职业规划方面有些困惑,所以想找些人聊聊。当时我突然想到陈天之前从事过区块链方面的创业,后来从里面退出来了,所以我就想向他咨询一下。我没有他的联系方式,但灵机一动我想到了从 Git 的提交记录里面找 Email,然后抱着试一试的想法给他发了个邮件说明了自己的情况和困惑。没想到他很快给我回复了,并很详细地告诉我他对于区块链的想法,还有如何判断自己是否适合一个公司,通过各种途径了解公司的相关产品来作为决策的依据等等。

我作为一个陌生人,陈天老师都会乐于给与指导和帮助,可见为人真的很好。还没能有幸和陈天老师现实中有所交流,我本来想用当助教的机会和陈天老师多学习,但时间方面安排不过来了。希望大家能在老师的的训练营学到知识、经验、还有探索技术的乐趣!

我喜欢的 shell 工具

作者 yukang
2024年3月17日 07:52

分享一些日常经常使用的命令行小工具,我认为这些小东西能提高我的工作效率。

percol

mooz/percol 这个工具是典型的 Unix 风格工具,它唯一做的事情就是通过管道接收输入,提供一个模糊搜索和 UI,用户选择后再把结果返回给后面的管道继续执行。

比如我这个 gt 的 alias 是我日常使用非常多的一个命令,做的事情就是 check out 一个 git 分支,因为我的本地通常有很多的分支,所以使用这个命令来模糊查找,然后选中就非常方便了:

alias gt="git branch| percol | awk '{ print \$1 }' | xargs git checkout "

类似的下面这个命令是 kill 掉某个进程,我们可以通过模糊搜索来找进程:

alias pk="ps eaux | percol | awk '{ print \$2 }' | xargs kill -9 "

如果你仔细总结,日常开发任何需要选择的地方都可以使用这个小工具来达到更高的效率,比如我工作的目录下有很多测试文件,测试其中一个文件的命令是 just ts file-path,我需要找到其中一个来测试:

find ./tests/ui/ -name \*.rs  | percol | xargs just ts

percol 可以嵌入到很多配置里面,比如在 tmux.conf 里面加入这个配置,这样可以模糊查找 tmux 的 session 和 window:

bind B split-window "tmux lsw | percol --initial-index $(tmux lsw | awk '/active.$/ {print NR-1}') | cut -d':' -f 1 | tr -d '\n' | xargs -0 tmux select-window -t"bind b split-window "tmux ls | percol --initial-index $(tmux ls | awk \"/^$(tmux display-message -p '#{session_name}'):/ {print NR-1}\") | cut -d':' -f 1 | tr -d '\n' | xargs -0 tmux switch-client -t"

atuin

atuinsh 是一个记录 shell 历史的小工具,不同于普通的记录 shell history 的工具,atuin 会把数据记录在一个 SQLite 的数据库文件中,这样可以支持更丰富的查询功能。

另外 atuin 也支持不同机器之间的同步,当然这需要加密通信。我目前还没使用这种场景,只是把 Ctrl-R 绑定到了 atuin。

atuin 也是一个 Rust 实现的工具。

tmux

tmux 我之前听很多人推荐过,但是我一直没怎么尝试,直到某天我需要通过网页打开跳板机登录到服务器上,网络不稳定的情况下我经常需要重新登录,这时候我尝试了一下 tmux 发现真是太好用了。

tmux 的教程很多,比如 Tmux 使用教程 - 阮一峰的网络日志。我的 tmux.conf配置很简单:

set -g @plugin 'tmux-plugins/tpm'set -g @plugin 'tmux-plugins/tmux-sensible'set -g @plugin 'tmux-plugins/tmux-resurrect'set -g @plugin 'tmux-plugins/tmux-continuum'unbind-key C-bset-option -g prefix C-Spacebind-key C-Space send-prefixset-option -s set-titles onset-option -g set-titles-string "#W/#T"run '~/.tmux/plugins/tpm/tpm'

安装 tmux-resurrecttmux-continuum,这样即使我重启了机器,打开 tmux 后我的 session 仍然和之前一样。

最近也有个 Rust 写的 zellij,但我认为这种软件使用更老的会更方便,比如公司的远程服务器必然有 tmux,但不一定有 zellij。

just

casey/just: 🤖 Just a command runner 是我喜欢的另外一个 Rust 写的工具,我的日常工作中严重依赖这个工具,比如我的 rustc-dev 项目中配置渐渐积累了这么多的配置:rustc-justfile

just 有些像 Makefile,但使用起来又比 Makefile 的语法简单和直观,我通常是来把一些常用的命令写入 justfile,然后留下经常需要调整的参数,比如:

err FILE N:        rustup toolchain link dev2 ./build/aarch64-apple-darwin/stage1/        RUSTC_ICE=/tmp rustc +dev2 {{FILE}} -Z treat-err-as-bug={{N}}

这样我执行 just err tests/ui/consts/const-eval/infinite_loop.rs 1 的时候就相当于执行配置的一系列命令。

另外我也会把一些频繁需要修改的参数放到最后一个位置,比如本来我需要执行:

CKB_TEST_ARGS={{SPEC}} make integration

在 justfile 里面配置:

test-one SPEC:        CKB_TEST_ARGS={{SPEC}} make integration

执行 just test-one SPEC 来测试不同的用例就会方便点。

其他


你有什么喜欢的 Shell 工具,希望也能分享给我。

中外程序员差异

作者 yukang
2024年2月25日 05:58

今天看到这个推特,我理解作者的心情非常复杂,因为我前三年写过一段时间技术类的英文文章,也发现英文技术社区还有那种认真讨论的氛围,而在中文技术圈里,这种氛围已经几乎绝迹。

这几年因为我在外企和开源上的工作经历,接触了很多来自各个国家的程序员,今天想写写我发现的一些国内外程序员间的差异,我相信经常混迹开源社区的人会有些类似感受。

讲究细节

这点是我感受最深的,我自认为已经算是一个对细节比较在乎的人了,但我接触到一些国外的程序员,他们对细节的把握让人佩服。

最近的一个例子是我在写这篇英文文档 的时候遇到的,其中的 Reviewer jordanmack 对我文档里面的所有内容逐字逐句都过了一遍,发现不懂的地方一定要弄明白。这里面有的是中英文表达差异造成的理解偏差,也有他对这个功能的逻辑上的质疑,甚至可以细节到我在文中给出的 json 例子里的数字范围和自洽性。我们在 Github 上来回讨论了很久,然后继续在 Discord 上讨论,而在这个过程中我也确实发现了些代码上需要调整的地方,最后他给我的文章几乎全部润色了一遍

jordanmack 不算是全职的程序员,但他也有一些程序员背景。在我做开源的经历中,PR 中被挑细节的时候太多了,一度我已经不再认为自己是个对细节很把握的人了。后来我总结了一下,有时候我是在赶时间,觉得某些 corner case 就暂且跳过吧,但大多会在代码 Review 中被提出来的。

然后经历多了也就看淡了,不光是我,任何人的 PR 都可能引发大量的讨论,比如到底是使用 µs 还是 us。也许在很多人看来这是个小问题,但却引起了大量的讨论,细看其中还有些引经据典和长篇大论。随便挑一个 RFC,也都可以看到大量的讨论

所以我的感受是,国外程序员中在意细节的比例更大。那么问题是,他们为什么能看这么细?固然其中一个很重要的原因是他们确实有时间,才能静下心来看和写。

在国内公司我也碰到过对细节的把握,但很多用在了我最讨厌的形式主义上。在微软的时候,我见过各种不够漂亮的 PPT,有的时候翻来一段 onenote 就开始讲,因为都没人关注这些。

文字表达

文字表达能力是开源社区里一个非常重要的,因为但凡一个大的改动都需要和其他人广泛讨论和协作。

不少国外的程序员有文字表达的习惯,就是即使看很小的一点问题也会通过文字表达出来。这是很多国内程序员所没有的习惯,因为我们大多比较含蓄,认为多做比说强,说多容易错,说多容易暴露自己。

可能和教育和网络环境也有一定关系,如果不是刻意维持文字表达的习惯,很多人高中毕业后就没有写过几篇长文,对很多事情也没有自己的看法。

另外他们习惯使用 Email 来沟通,但中国开发人员大多习惯使用 IM 沟通。这两者还是有区别的,IM 沟通会让人不自觉地回复得更快,有的模糊想法随口就就表达了。而 Email 沟通更容易让人把事情写清楚,也更容易写得更长和有条理。

这种细微的差异长久了之后就可看出中英文技术社区的巨大差别。另外,中文网络的环境中戾气更重一些,人们对自我推销很反感,容易揣测你的意图。

个人目标

很多欧美大公司里有不少只做 Individual Contributor 而不做管理的人,在这些公司里,管理和技术是两条并行线,薪资和职级挂钩,也就是说纯 IC 的岗位可能收入比管理岗位更高,因为职级更高。

管理人员和技术人员大多是上下级关系,但下属对管理人员没有绝对的单向服从关系。当然大多数情况下,管理更容易升职上去,因为纯做技术岗位不容易通过杆杠来放大自己,管理就是一种很有效的杠杆。但这种纯粹的并行晋级路线是非常重要的,可以让技术人员有更多的选择权,甚至如果对自己的管理者不满意直接给差评和换组就是了。

所以在国外程序员中,如果一个人做了多年开发,很可能就是他确实喜欢做技术和更擅长做技术。而中国职场中,管理和技术岗的差别太大了,或者说绝大部分人到了一定年龄,如果你不混个管理的 title,好像就已经落后了,甚至没有职场安全感。

另外有些人是喜欢混到管理岗之后,纯粹为了获取更高的薪水,或者是为了把不喜欢做的事情推给别人。当然,这其中也有很大一部分中国文化里的官本位的影响,还有一部分原因是太看中钱了。

我接触过一些年龄在 40 岁多的国外程序员,他们还是对技术有很大热情。如果喜欢做技术,而又能通过做技术挣钱,这没有什么失败的,这与年龄没关系,反而这是一种很好的度过自己短暂一生的方式。

只是在国内要做到这点并不容易,很多岗位做的事情本来就不够有深度,时间更久也无法积累起来足够的壁垒,业务上的开发年轻人上手很快,而需要深入做下去的岗位不够,所以年龄大了就容易失业。

业余时间

国外程序员的业余时间真是非常多,如果你经常混 Github 就会发现,每当到了 12 月份就很多出来很多 aoc 字样的项目,这是他们在做Advent of Code 2023

Advent of Code 就是整个 12 月份每天出一道题目,都是些编程谜题,有点类似 leetcode,但题目描述更长。你可以用任何语言来实现,反正要的结果就是答案。可以发现这些 aoc 项目基本都是欧美的程序员在做,因为他们大多在 12 月份有几乎一个月的假期,我在微软工作的时候,很多人也是 12 月份开始基本不见人影。

创造性的前提是不用为生存问题发愁,欧洲那些搞哲学、做研究的,大多都是家底丰厚,闲得多了自然就能搞事。如果有大把的业余时间,用来发展工作外的开源项目可就太好了。其实很多著名的开源项目只有吃饱饭没事做的时候才能搞出来,我之前在知乎问题 为什么中国程序员不如外国程序员有创造性 中写到:

荷兰人蟒蛇大叔想着哇塞圣诞假期这么长,找点事做,结果出了 Python。
日本人松本行弘,经济危机时闲得发慌,搞出了 Ruby。
芬兰人李纳斯,大三不用为了找工作背八股和考研,冬眠似地宅家里写代码,搞出了 Linux。

在 Rust 社区中的贡献人员里,除了美国,第二多的是欧洲,他们也不是为了挣钱,完全就是感兴趣做做而已,我看到好几个大学年轻人做的事情已经非常深入了,当然其中花费的时间也是很多,他们几乎一直在线。


这些总结比较粗略,另外也可能有幸存者偏差因素。这个话题很大,深入下去探讨会包含很多方面。

我也不是在抱怨,年龄大了之后发现多看看历史相关的书还挺好,让自己更容易理解所处的环境为什么是这样的,比如《中国国民性演变史》这本书值得推荐。另外《美国种族简史》这本书也值得一看,多了解了解其他人的特点和长处,努力让自己不要局限于国界,另外做到程序员中的 80% 以上水平,保持英文能力。

Copilot,最好的编程助手

作者 yukang
2024年1月21日 22:43

今天下午我解决一个小问题的时候,在 Copilot 的帮助下快速给出了修复,这个工具似乎有些超过期望了,所以突然想写篇文章分享这个目前我最愿意付费的 AI 工具。

Copilot 价格是每个月 10 美金,但我至今还没付费过,感谢微软支持开源,从测试阶段就邀请我试用,到现在还一直在免费使用。Github 应该有些政策,比如如果你持续给一些 star 数比较多的开源项目做贡献,就可以免费使用 Copilot

我会给出日常碰到过的一些具体的实际案例截图,以方便你更直观地感受到这个工具准确度。

Manual 类查询

我们在编程过程中经常会碰到一些命令的参数记不太清楚,这种问题很适合问 Copilot。这比自己去 Google 的感受好很多,因为他几乎能完全理解用户说的自然语言,而且给出的答案简介明了:


比 Google 更好的地方在于上下文的交谈,比如我继续基于上面的问题说我的想法,他就能继续给出反馈,比如我说大概有个类似 --exact 的参数,Copilot 会继续给出使用案例。

Copilot 非常善于回答对这种 manual 类的问题,因为这是有标准答案的,并且我作为用户对这些是有判断的,只是我们细节上记不清楚了。

还有一次我发现跑测试的时候挂了,分析下来是这个命令行失败了(但既然 CI 是过的,所以必然只是在 MacOs 下失败了):

diff -u --strip-trailing-cr -r -q A_file.txt A_file.txt

这是在 diff 同一个文件,所以必然应该返回 0,但在 MacOS 下这个命令会报错:

diff -u --strip-trailing-cr  -r -q ./x.py ./x.pyerror: conflicting output format options.blah blah 一大堆错误 blah blah 一大堆错误 

我知道这里面肯定是有参数冲突了,但我具体不知道是哪两个冲突了,所以这时候我问 Copilot:

可以看到这个解释非常清楚,并且帮我找到了问题的根源,所以我就能很快地发 PR 修复这个问题,并且我 PR 里的描述基本都是从 Copilot 里来的:
Fix diff option conflict in UI test #109036

给出示例代码

我们在写代码的时候,经常会出现固定的 Pattern,不同的语言对固定的 Pattern 有一些相对固定的代码样式。我很喜欢找 Example 类的代码,然后在这个基础上再思考或者修改:

对这种情况我们需要给 Copilot 足够的信息,他给出的 Rust 代码通常是可直接编译通过的,但当然这些示例代码需要进行仔细的修改,但这也比我自己翻 Doc 会快很多。

辅助排查问题

VSCode 上的 Copliot 更新很快,肉眼可见地体验越来越好,现在我们可以选择一段代码,然后就选择的代码来进行提问。

有时候我会选中一个函数,然后问这段函数能不能重构得更简单一些,或者我们能不能用其他方式实现。

今天让我有欲望写下这篇分享的文章是因为这个问题:
Missing request extension: Extension of type

这是一个有非常明确的报错的繁琐 issue,应该就是 Server 端限制了 HTTP 的请求类型,客户端通过 curl 发 GET 请求的时候报错了,只是这个报错信息看起来很不友好,而且和老版本行为不同。所以我就选中代码中对应的函数,然后问这里为什么会有这个错:


其实我对 Copilot 解决这个问题不怎么报有信心,只是好奇先试了试,没想到 Copilot 真的能理解我的代码,并且指出了问题所在。注意看它加的注释就是我代码中缺少的逻辑 (之前的代码只是在 enable_websocket 的条件下才加载了 stream_config 这个 Extension):

加上它建议的代码之后,那个错误信息没了,但是现在发 GET 请求是另外一个问题:

Connection header did not include 'upgrade'

这看起来是服务端期望客户使用 Websocket,但是客户端只是在通过 Curl 发一个 GET 请求,并没有按照这个期望来。所以我继续问 Copilot:

他给的回复里的代码并没有直接修复问题,但里面的
you can separate the handlers for POST and GET requests
提示了我应该尝试对 HTTP endpoint 和 Websocket endpoint 的 handler 进行分开,所以我一下想到了修复方案:

总结

如今使用 Copilot 已经成为我的一个编程习惯,就如同之前我严重依赖 Google 一样,但这个工具明显比搜索引擎高级了一个维度,当然现在我还是依赖搜索,但使用比率明显下降了不少,搜索引擎更像是成了一个书签的角色了。

我之前认为 Copliot 这种工具甚至是这辈程序员所不能体验到的东西,在我第一次尝试到 ChatGPT 居然可以理解一个函数,并且找出函数中的问题时,就感觉新的编程时代来临了。

前段时间 Redis 的创始人在文章 LLMs and Programming in the first days of 2024 中写到:

随着时间的推移,我们见证了框架、编程语言、各种库的大量涌现。这种复杂性通常是不必要的,甚至无法自圆其说,但事实就是如此。在这样的情况下,一个无所不知的“白痴”成了宝贵的助手。

这是一个事实:现今的编程大多是在微调同样的内容,只是形式略有变化。这种工作并不需要太高的推理能力。

Copilot 已经可以在一些具体的编码问题上给到我们很多帮助,甚至你把这个当作一个包含万物的文档查询工具都非常有效。

当然没有银弹,Copilot 并不能解决编程中的所有问题,比如理解大规模的程序,通过深入分析去找出 bug,或者做设计问题中的各种折中和取舍,这些都是不能取代人类的,这也是我认为编程中的乐趣还没有完全消失。

我会把繁琐和细节的问题抛给 Copilot,然后更开心地做重要和有趣的部分。

我的 2023

作者 yukang
2023年12月31日 06:36

2023 年很快就要结束了,赶紧抓住这个冲动总结一下。今年对我来说有几个大的转变,从几个方面谈起:

生活

生活上最大的变化是我又有了一个儿子,所以我现在是三个孩子的父亲了。

同龄人中几乎没有生三胎的,有些人问我为什么这么想不开,自己找罪受。我只能说是命运的馈赠吧,我从小生活在一个大家庭里,加上我和老婆都算是喜欢小孩的人,三胎顺其自然地接受了,这个孩子也促使了我们更早地离开了苏州。

孩子 8 月底出生,前两个月请到了一个靠谱的月嫂,所以生活方面还不算痛苦。最近女儿生病才开始感受到三个孩子带来的巨大挑战,看来我们是低估了其难度。

我的大女儿开始在深圳上一年级,没想到现在的一年级都这么卷,基本上每天都有语数外作业,一个月一次的考试。我们力不从心已经放弃了一些家庭作业,比如数学之类的无聊作业我们就不怎么做,我认为每个小孩的大脑发育有自己的节奏,小学数学这种东西到了年龄自己会懂,小学阶段重要的是培养学习习惯和兴趣,强压给孩子只会让她产生对数学的恐惧。陪小孩做作业真是一件极其需要耐心的事情,我现在还在努力尝试从孩子的角度考虑问题。

三个孩子带来的另外一件事情就是冲突,大女儿心情好的时候会带着小的玩,心情不好的时候就会和妹妹争东西。如何在这些孩子中平衡,在吵闹中克服情绪去解决问题,这些都是在磨炼心性。

纪伯伦在《论孩子》中写到:你的孩子,其实不是你的孩子,他们藉助你来到这个世界,却非因你而来,他们属于你做梦也无法达到的明天。

有孩子之前我觉得养育孩子重要的是把他们当朋友,但真的等孩子三岁后有了更多自主意识之后,作为父母就会面临更多困难,什么时候该管教孩子,什么时候该放任他们。有时候我也忍不住发火,而后又觉得自己是个失败的父亲,心里多默念『还只是个孩子』几遍,如何做一个好父亲这必然是我今后一直需要学习的。

生活中的另一个变化是今年身体状态更好了,可能是因为深圳的暖和天气更适合我,加上在家办公出去本职工作外,没感受到什么职场上的琐事和压力,另外在家里办公相关的设备更适合自己,所以整体身体上没有大的问题。

但从心理方面,我能感受到和以前的更大差别,主要是彻底接受了中年这个年龄阶段。这是一点点积累起来的,那些曾经我看着长大的晚辈们都到了谈婚论嫁的年龄,或者偶然想起一些人和事心里一算已经是十多二十年前了,或是我发现自己某些方面更像印象中的父亲了。

我的生活看起来极其单调,不是坐在屏幕前写程序就是在带娃和遛娃,和梦想与激情这些词汇毫不沾边。但我满足并感恩目前的状态,我几乎没有焦虑,物欲低所以也不觉得缺钱,做着自己喜欢的工作和事情,有足够多的时间陪家人,这就很好了。

工作

2023 我全职远程做开源项目,很幸运在 Cryptape 这大半年里工作感受非常好,这大概是我工作这么多年来写程序最开心的一段时间。因为远程办公,今年我的人际圈子似乎更小了,日常微信沟通的都是些认识了 10 来年的朋友。

我的工作主要是做区块链 Layer 1 相关的事情,入职以后做的事情是交易池这块,后来又涉及到一些 RPC 相关的工作,还有些 Infra 类的工作。区块链这行涉及范围太多了,有网络、性能、分布式、密码学等各种,所以对于纯喜欢技术的人来说,这里面挑战太多,比 CRUD 之类的项目好玩得多。

我做的主要工作都是集中在 nervosnetwork/ckb这个项目,这里可以看到我做的一些 Pull requests

另外现在日常工作中纯用 Rust,编程体验和之前完全不是一个层次,除了如何实现功能,我们也会在乎项目的长期可维护性和优雅程度。区块链 Layer 1 也算是一个复杂度高和对准确度要求很高的项目,Rust 是很适合的。我虽然这两年一直在写 Rust 代码和做开源,但之前还真没有用 Rust 在实际工作中,特别是异步这块我之前甚少涉及。同事中有对 Rust 理解很深入的人,沟通也很顺畅,所以我特别喜欢这个工作氛围。

在工作过程中我看了更多 Bitcoin 相关的代码,越发觉得这真是一个伟大的发明,这像是个黑客用技术发起的社会性实验,在 beta 阶段就能如此深刻地影响了世界。关于 Bitcoin 推荐看这一系列文章 比特币的过去、现在和未来

开源

今年继续在为 Rust compiler 做贡献,能回想到的一些事情是:

因为在 Cryptape 的工作涉及到其他一些 Rust 项目,所以参与到了一些,比如我们在改造交易池的过程中用到了 multi_index_map 这个数据结构,顺带完善了一些不足 Non-unique index support, capacity operations, performance improvement

作为技术人,能全职使用自己喜欢的编程语言工作是一个很大的幸运,希望能继续在 Rust 开源这条路上走得更远。

阅读和写作

2023 看书的时间也少了很多,回顾了一下很多书没有看完,但这些书看完后值得分享:

  • 《硅谷钢铁侠:埃隆·马斯克的冒险人生》,这就是那些改变世界的人吧
  • 《失明症漫记》,似乎是重新回顾一遍疫情的场景
    • 如果我们亵渎生活的尊严,我们也就扭曲了理智;而人的尊严每天都会受到我们世界中权势者的侮辱;普遍的谎言已经替代了多元的真理;人一旦失去来自其他成员的尊重,他也就不再尊重自己。
  • 《作个闲人:苏东坡的治愈主义》,这书我估计我年轻的时候看不进去,现在看就觉得很好:
    • 人生如逆旅,我亦是行人
    • 可以寓意于物,而不可以留意于物
    • 一张琴,一壶酒,一溪云
  • 《走出戈壁》优秀的人在逆境中也能成长起来。
  • 《了不起的盖茨比》也许是因为我先看了电影,所以再看书就满脑子小李子那样子,也许有的作品就不应该看电影。
  • 《被讨厌的勇气》,一切烦恼都来自人际关系,让干涉你生活的人去见鬼,解决了一些我的日常困惑。
  • 《哲学家们都干了些什么》,你思考过的很多问题,前人必然已经思考过了。
  • 《夜晚的潜水艇》,这就是文笔好。我喜欢里面的《裁云记》
    • 值得人沉迷一生的事太多了。像你说的,每个洞穴都充满诱惑,难以取舍。我年轻时也在分岔处犹豫过。后来我才明白,不是所有洞口都陈列在那里,任人选择;有的埋伏在暗处:我一脚踏空,就一头栽了下来,到现在也没有落到底。
  • 《美国种族简史》
  • 《高山下的花环》
  • 《凤凰项目,一个 IT 运维的传奇故事》

同样在写作上的时间就更少了,总结下来居然是 13 篇博客,勉强达到月更的节奏。

写作这件事情似乎停下来之后就容易长时间停顿。带孩子太耗精力算是一个借口,但我其实很是可以把一些日常的琐碎时间利用好来做这件事情的,只是确实犯懒了。

希望借这次写年终总结的劲头,把写作这件事情捡起来。

Andriod 使用 Obsidian 的客户端

作者 yukang
2023年9月19日 17:16

上周末试着在 Andriod 上配置好了 Obsidian 的客户端,没想到还挺好用。如果你已经买了 Obsidian 的 sync 服务,并且一切用起来都挺好的,那就不用看我这篇介绍了。

我折腾这个的主要的需求是使用私有仓库的 Git repo 来同步日记。为什么不买 Obsidian sync,我认为 Github 更符合我的使用习惯,并且我选择使用 Obisidian 的一个原因就是我不想把笔记数据同步到其他的第三方平台上,相对来说 Github 是我更信任的基础设施,毕竟我已经使用 Github 这么多年了。

Andriod 客户端

Obsidian 的安卓客户端好像没有在国内各个安卓软件市场上,你需要用过 Google play 来安装。

Termux

termux/termux-app 是一个 Andriod 上的终端模拟器,也是一个开源软件。基本上你可以把 Andriod 当作一个简化版本的 Linux 服务器来使用,Termux 高级终端安装使用配置教程 是一个很详细的介绍文章。

注意目前 termux 已经不能在 Google Play 上安装了,你需要去 Releases · termux/termux-app 下 apk 安装包来手动安装。

termux 安装好之后就可以在 Andriod 手机上跑一个 Shell,打卡进去之后运行来创建一个叫作 storage 的目录:

termux-setup-storage

接下来安装一些后面需要用到的依赖:

pkg install gitpkg install openssl

Git

ssh-keygen 来生成一对公钥和私钥,把公钥配置到自己的 Github 账户上,然后 clone 你的 Obsidian vault repo:

cd storage/sharegit config --global credential.helper storegit config --global user.email "<your_email>"git config --global user.name "<The name you want on your commits>"git config --global pull.rebase true

确保能在 Termux 上正确提交改动到 Github 上。如果有一些文件是不想同步到远程的,可以加入到.git/info/exclude 里,比如把 .obsidian/workspace-mobile.json 忽略了。

打开 Obsidian 的客户端,找到刚才 Git clone 的目录,打开作为 vault 即可使用。

定时备份

先安装 Termux 上的 cron 服务:

pkg install cronie termux-services

然后退出 Termux 重新打开,运行:

sv-enable crond

运行 crontab -e 来创建一个定时备份的 job:

*/2 * * * * ~/sync_repo.sh

每两分钟自动备份一次,我的 sync_repo.sh 是这样的:

#!/bin/bashcd  /data/data/com.termux/files/home/storage/shared/obgit add -A && git commit -a -m "android backup: `date +'%Y-%m-%d %H-%M-%S'`"git pullgit add .git rebase --continuegit push

上面的同步脚本很粗暴,如果冲突了我会把冲突一起提交进去,但这也是合理的,因为我需要让自动同步尽量成功,至于冲突可以在笔记本上解决。如果 Termux 进程被杀了,自动备份将无法自动运行。但在我的日常使用过程中,这倒不是一个大问题。


参考:

读《走出戈壁》

作者 yukang
2023年9月14日 07:55

最近读了单伟建的《走出戈壁》,作者是个著名的金融家、投资人。我对这位作者不熟悉,但看完后觉得非常好,所以推荐给大家。

这本书记录了作者从小时候的经历、年轻时的知青生活、美国求学的回忆,算是一部回忆录。原版是用英文写作的《Out of the Gobi: My Story of China and America》,但中文版据说不是直接翻译过来,而是很多部分重新用中文写出。我猜还有一些英文版的内容如果翻译过来,那这本书就不能出版了。

知青下乡的书我看过一些,最早的时候是偶然发现家里有一本叶辛写的《蹉跎岁月》,所以就看了起来。那个年代于我这种 80 后而言很陌生,但小说中人物的某些心理特征对那时候的我来说很熟悉,比如羞涩和自卑等。这本书我后来又陆陆续续看了几遍,里面也有些爱情心理的描述,算是我看得比较投入的第一本小说,所以至今仍然印象深刻。

王小波也写过不少关于知青岁月的文章,那个年代里每个人都背负时代的枷锁,出身和成分很大程度上决定了人的命运,大部分人因为十年的浩劫失去了接受教育的机会。

有些自传和回忆类的书读起来会有点自吹自擂的感觉,而这本书里作者用了一种看似云淡风轻、带着些许幽默的口吻写成,而细节很丰富,阅读中仍会让人感到戈壁凌冽的北风,那样的生活真是太苦了。

一群年轻人被放在了戈壁滩上,总得找事情去让他们去做做,完成自我的”改造“,日常生活就是饥肠辘辘地”修理地球“。里面有不少这类事情,让人很辛苦地去做完而结果看起来没什么意义,比如部队让大家去挖土豆,结果挖出来又没开车去收,大部分又烂在了地里;比如让大家去修土壕,然后无止境地半夜做演习,结果少部分人被埋死在了土壕里。

这样折腾几次之后,大部分人都会“看破红尘”,开始随大流地磨洋工,而作者的心态是”干什么事都要干好,否则闲着也是浪费时间,而且争强好胜,虽然身体瘦弱,但不甘人后,如此而已。“

没什么英文资料可读,就反复看药品说明书里的英文单词。积极认真的工作态度,抓紧时间学习一切东西,因为这些他才能后来被推举成为工农兵大学生。一个让我印象深刻的是他处理人际关系的方法,当遭受到他人的算计时,并未过多抱怨他人,而是认识到这本就是人性中存在的恶,然后从自己的角度去尝试解决这些问题。他开始了一个广结人缘的计划,还能用一些看起来很隐蔽的方法,比如让父亲寄书过来学着当排球裁判,让更多其他连的人认识他。能站在旁观者的视角审视自己和周遭处境,并找到解决办法,这对于一个 20 岁左右的年轻人来说是非常难的。

作者在前两年的大学生推举中仍被刷下去了,大致是因为和领队的关系不够密切。这两次对作者来说是很大的打击,所以抗搓能力非常重要,即使非常难过也得在人面前保持平静:

  • 📌 漫无边际地走,一边走,一边放声大哭。
  • 📌 那天晚上,我很晚才回到宿舍。我告诫自己,不能放弃,无论受到多大的挫折,都不能放弃,放弃就是对自己的犯罪。我必须坚持下去,继续努力,等待下一次机会。

所以说,那个时代的大学生,不是纯考试的,但能通过群体推举去上大学生,绝对是非常不简单的人。经历过那个年代的苦之后,以后什么学习上的苦都是不足挂齿了。作者上大学之后,学习和成长的速度都是惊人的,10 年间从一个戈壁知青做到了藤校的副教授级别。他到了美国之后在两三周之内就能说服校方和教授,为自己定制了一个特殊的学习路径,并在两年内拿到硕士学位:

  • 📌 我承认,美国体制的灵活性对于我来说是如鱼得水。在国内的体制下,当初上大学有如登天,几乎没有自由选择的权利。
  • 📌 在最心灰意冷的时候,我反复提醒自己,自我放纵就是对自己的犯罪。基于这个信念,我从未放弃,而是坚持不懈地努力,刻苦读书,才有了今日。

其中的一个感人的故事,教授夫妇发现作者很想拿一个学位而且学习能力强,但是没有足够多的钱来应对学业开销,所以就谎称说有人匿名资助了他,其实就是教授夫妇自己资助了他。我猜想也许是因为作者赶上了新中国第一波留美学习的时机,不少人对他们是有好奇心态的,或者是“自助者天助之”。

还有一个小故事说明作者深谙体制里那套规则,并且做事很有智慧,他是通过基金会的一个留学考察项目去美国的,所以读硕士学位其实并不在计划内的:

后来我明白了为什么亚基会的官员们不热心,他们担心如果为我破例的话会影响基金会与外贸学院的关系。安迪表示,他要给外贸学院的领导写一封信,征求北京的意见。

我说你不能这么写,他问我为什么。我说,如果你征求北京方面的意见,他们就要研究是否批准。只有两个可能——批准或者不予批准。批准了当然好,但是如果不予批准,我怎么办?安迪问我还有更好的办法吗?我说有,你就给北京发个贺电,说我学习成绩优异,校方决定给我奖学金,只需延期一个学期,就可以获得硕士学位,对于这样的成绩,亚基会向外贸学院表示祝贺,其他的都不必说。

安迪将信将疑地接受了我的建议,草拟了一封电报,赞扬了我,把我的成绩归功于外贸学院的领导知人善任,表示祝贺。两周后,安迪打来电话,说外贸学院回电了。“怎么说?”我焦急地问。他停顿了一下,说:“只有四个字——‘非常感谢’。”我心花怒放。

正如我所料,谁能拒绝别人的道贺呢?后来,外贸学院的领导还专门给我写了一封信,对于我在美国的学习成绩表示满意,鼓励我再接再厉,早日拿到学位。

看这本书的过程中,我会想起自己最努力的初中时光。那是我第一次读寄宿,学校的物质条件也很贫乏,每次下完课去吃饭都得百米赛跑,不然自己的饭就会被瓜分掉。另一个深刻的印象是冷,热水也总是需要抢。生活虽然清苦,但那几年我开始感受到学习和思考的乐趣,上自习做到半夜也不觉得累,我当时觉得几何证明题目很有趣,第二天早上五六点又会爬起来去教室里早读。回想起来,我后来再也没那么专心和努力过了。

匮乏和苦难也许真能磨砺人,在那样的大环境下如何生存,在逆境中保持乐观、有所成长,这本书里所描写的是绝大部分人无法做到的。可以修改一下长者那句话:一个人的命运啊,当然要考虑到历史的进程,主要还是靠自我奋斗。

成功申请 Rust Foundation 2023 Fellows

作者 yukang
2023年8月13日 07:00

很高兴成功申请到 Rust Foundation 2023 Fellows,我认为自己投入到 Rust 之后运气很好,两次申请到开源上的资助,很幸运能够在 cryptape 技术氛围这么好的环境里远程做开源项目。

也许是因为我践行了创造运气的方法:多做 + 多分享,所以顺便多分享一些其他想法。

三年前,我离职时不知道自己未来如何走技术路线,在国内很多职位都很卷,当时我对工作和环境都厌倦了,所以离职换了公司和城市。我在微软的岗位虽然是技术的,但做起来比较无聊,好在业余时间比较多。

闲暇能激发创造力和保持动力,很感激前公司微软包容和良好的工作环境,在苏州的两年里我有时间和精力去找自己感兴趣的事情,我重拾了写作和开源,这让我的生活变得充实,又因为些巧合开始给 Rust 编译器做贡献,从中得到了很多乐趣和收获。

没想到后来逐渐走上了 Rust 开发这条路,再回想起来过程也算是漫长的了,从 2014 年开始接触 Rust 到如今完全以写 Rust 为生,从观望学习、业余投入、全职投入这个过程快 10 年了。回想起来我对编程语言的兴趣是从看 eopl开始的,而再往前是因为 scheme,再往前是因为偶然用了 emacs,所以年纪越大越觉得这句话太对了:

You can’t connect the dots looking forward; you can only connect them looking backwards. So you have to trust that the dots will somehow connect in your future.

Rust 让我开启了一个正向循环,比较容易能在一些开源项目上做出贡献,能看到开源社区里面有很多其他和我类似经历的人。

Rust 社区里另一个吸引我的地方在于有很多乐于分享的技术人,我们可以从世界上这些优秀的工程师身上学习,比如最近我喜欢看 Jon Gjengset的频道,他一期直播就有 2、3 个小时,讲解得非常细致,涉及到的主题也非常宽泛,有分布式、Rust、算法、读博和生活上的体验等等。

当然长期做开源很难,只有真的热爱才能持久,而对喜欢做技术的人来说开源是可能会上瘾的。这一年多里,给 rustc 做贡献好像已经成了我的习惯,业余时间找个 issue 试着解决就像是玩一把游戏,所以我做的事情大多并不难,而只是需要时间和耐心。国外程序员的一大优势就是有空余时间,生活负担不大,并且有足够的耐心去做周期长的事情。

这些资助算是兴趣的副产物,这次 Fellow 项目的一个好处在于可以找 mentor 来指导自己,我还在尝试找打算做的具体领域。上次 Project Grant 让我尝试了些新鲜事,比如录制播客、在 Conf 上公开演讲,这些都是代码之外的一些体验:

感觉上面的播客和演讲的内容大多差不多,打算以后会在博客里再写点技术细节相关的博文。

做得越多,越觉得自己还有好多东西需要学习,我算不上编程语言的专家,只是个业余的爱好者,希望能在接下来时间里有更大的成长。

远程办公的体验

作者 yukang
2023年8月6日 19:15

我断断续续也有好几年的远程办公经历了,从疫情刚开始那会儿,我还在大疆工作,在家办公一个来月。2020 年下半年开始在微软,因为疫情那两年一直反反复复的,所以公司长期都是混合式办公,我通常每周会在家待两天,到了 2022 年大部分时间都是在家办公。目前在秘猿则是完全远程办公,我还从未去过公司办公室 :) 。

远程办公有好的方面,也有一些需要克服的困难,这篇文章我总结一下这方面的想法。

前提条件

并不是所有团队都适合远程办公,因为有的岗位需要频繁交流,而面对面沟通肯定是会更高效的。就软件开发这行来说,如果公司要实施全远程办公,需要满足几个条件:

  • 有效的管理、分工和协助
  • 员工有足够的自驱力
  • 公司的 IT 支撑

这些缺一不可,下面我稍微解释一下。什么是有效的管理,通常员工人数到了一定规模都会强调管理,然而很多公司做的是过程管理而不是结果管理,比如统计员工的加班时长,这是在衡量员工的上班过程,也许是因为没有更好的办法来衡量产出、或者是为了压榨员工,反正这就是一种管理上的失败。

远程工作本质来说就是放权,关注结果而非过程。就纯软件开发这行来说,工作结果是相对容易评价的,比如功能是否高质量地完成,方案和设计是否合理等,项目进度等等。

如果公司让员工远程办公,意味着相信员工能够自己管理好时间和进度,而公司也有合理地方式来验收结果。这要求员工有足够的自驱力,而且员工也对工作内容有足够的兴趣,如果一个人对工作内容没什么兴趣或者是排斥的,那远程的情况下就会更糟,因为人都是有惰性的。

远程最大的困难当然在于沟通,所以一个员工日常需要沟通交流的人数非常重要。通常一个小组就是日常协作的单元,15 个以内是相对可行的范围。对管理者而言可能是个更大的挑战,对 IC 来说日常沟通的人数通常是 4、5 个人以内。

另外公司的 IT 支撑很重要,如果工作中涉及到机密文件,而对应的 VPN 等工具不够完善,在家工作就会成为灾难。比如疫情开始那会儿,我的远程办公体验就非常不好,需要连入公司的网络才能访问某些文件,而且速度不稳定。微软的 IT 工具好用些,但连入生产环境和服务器之类的会非常麻烦,需要专门的另外一台笔记本来操作,用一台价值一万多的笔记本来专门连 VPN 确实很浪费,但这也可以理解,毕竟安全对于 ToB 的业务是更重要的事,只是日常带两台笔记本实在会很麻烦。秘猿的 IT 是专门为远程考虑过的,比如公司不提供办公设备,但会提供一些 IT 补助,员工可以用自己的设备来办公,代码都在 Github 上开源的,所以办公体验非常好。

异步沟通

异步沟通需要时间去适应,也更适合我这样的偏向于文字交流的人。在不能得到及时反馈的情况下,就需要把一个事情尽量用简单直接的文字把事情写清楚,并且需要考虑到对方可能会缺失的信息。当然,实时的文字沟通也是很重要的,适当地交流工作之外的事情,可以和线下没见过的同事培养出默契和情感。

软件开发中要达成有效的异步沟通,写好文档尤为重要。这对很多开发人员来说是一个重要的挑战,相比而言程序员更想写代码而不是文字,但文字和图都是更大范围表达自己的工具,你可以写给同事看,也可能需要写给用户看。如果无法通过文字表达清楚,意味着还没想清楚,那用代码也无法表达清楚。

所以,有的公司在面试时会看看应聘者是否有 Blog,主要也是想看这个人有没有文字表达的习惯,以及能不能把事情写明白。

就工具方面,我觉得邮件是个很好的方式,但相对来说国外开发人员更适应邮件。Slack 和 Discord 也不错,但感觉 Discord 相对来说更实时些。文档协作工具比如 Notion、Office 365、飞书等都行,我认为只要能有协同编辑就好。

除了异步沟通之外,一些在线的会议也是非常有必要的,但不宜过长,而且最好在开始之前让参与的人都了解会议的主题和相关资料,这样会更有效率。

我认为自己完全适应了远程办公的阶段就是培养出用文字记录工作的时候,我每天在 Obsidian 上都会建一个当天的文本,按时间顺序记录自己做的什么事情,或者是参与的会中的一些要点。然后通过标签做一些标注,还有把 Todo 给汇总到固定的页面。我在开会之前都会用文字记录会上要谈的要点,也会把一些零散的感受记录下来。

工作效率

通常来说,一个人的有效办公时间是到不了 8 个小时的,所以在办公室里面一直耗着就是耗着,很多时候都在摸鱼耗时间,有的会议就是大家都在摸鱼。

远程办公可以更好地利用碎片时间,以我的感受来说,如果是办公室办公通常下班后我再也不怎么去思考工作上的事,因为上班和下班是有一个明显的界限的。但在远程办公模式下这个界限会很模糊,可能我这会儿在办公,一会儿下楼去取快递了,或者我需要去接孩子之类的。所以很多时候,虽然我不在屏幕前,但我也会脑海里在考虑些工作上的问题。我反而觉得这种情况下会有更多的想法冒出来,比如人在洗澡的过程中会迸发出新的想法,这是很多人都有的体验。

这也是我在做开源的过程中得到的一个体会,因为大项目的开发者会遍布各个时区,所以绝大部分时间都是异步沟通。如果我彻底理解了一个问题,我就可以离开屏幕,随后时不时地去考虑这个问题了,等有了想法再回到屏幕前继续。

当然,在和他人讨论的情况下也可能会冒出想法,但总体而言群体讨论主要是为了达成共识,而更多好的想法是个体产生的。

在家办公一个影响效率的因素是环境,调皮的孩子可能是一个工作上的干扰,但我觉得问题不大,这也需要和孩子协调好。有时候我晚上思维更清晰、效率更高,因为晚上没人打扰,自由安排的情况下我可以更多地利用高效率时间工作,所以我在远程办公的情况下效率倒更高点。

对于大公司而言,整体来说远程办公的总体效率估计还是有损耗的,这也是硅谷大公司想让员工回到办公室的一个原因。但很多人宁愿少拿一些钱也想继续远程办公,因为就个人来说可以节省很多不必要的时间成本,这对于有孩子的员工来说真是太重要了。

心理问题

我看到很多人说远程办公会很孤独,我对此感受不够强烈,大概是因为日常经常和家人在一起,而且我还有两个小孩。如果没有小孩我估计也会感到孤独,即使结婚后也是容易孤独的,而和小孩相处完全是另外一种模式,我想这也是人类生小孩的一个重要原因吧。

但远程工作之后,确实会有一种脱离感,就好像没有进入社会的正常节奏。

脱离感也来自社交圈更小了,和同事之间的沟通基本发生在线上,而除此之外认识其他人的机会也少。我能想到的一个办法就是约老朋友线下见见,或者主动约一些线上认识的人聊聊,也可以线下见。总体而言,年级越大好像越容易产生孤独感,不容易深交,平时交流的人也都是些认识了十年、二十年的人,我不知道其他人是不是有类似体验。我倒是发现自己全职远程后,真的见到线下朋友会更有交谈的欲望,这大概是憋出来的。

另一个感受是,我远程工作之后倒更不容易焦虑了。细想一下大概是我抛去了办公室的一些不良因素的干扰,比如同事之间的竞争等。不是说远程没有晋升的压力,但我觉得从心理上我不是那么在乎了,而更在乎的是如何做好工作和如何提升自己,因为远程的情况下好像自己和公司更为平等的一个状态。不把自己和公司绑定,从物理上做到了就更容易从心理上达成这点。

如何找远程工作

最近确实能看到大量工作在流失,我身边失业的人越来越多,在这种情况下找远程工作只会更难,但这并不意味这没有机会。

我之前总结过一些工作,但我知道现在这个列表里很多公司都没在招人了:
remote-jobs-cn: 国内远程办公职位

这里有一个更全的、看起来还在更新的列表:
remote-jobs-in-china: 支持远程办公的中国公司

如果英语足够好,可以尝试找一些国外的远程机会,我在 Linkedin 看到还是有些的。区块链这行现在是熊市,所以工作机会也少了很多,但远程工作的比率相对大,大概是这个行业的人确实在践行分布式和无中心化,Web3 Jobs: Blockchain 这个站点上会有相关工作。

我能想到的另一个途径是,尝试找一些招人的、自己感兴趣的开源项目 (背后有商业公司运作的,可支持远程的) – 这样的项目也挺多的,然后给项目做贡献和社区的人熟悉,逐渐成为远程员工。虽然过程会比较耗时,但这确实是个途径。


远程办公还有很多其他好处,比如我现在可以把车的油耗保持在 6L 左右,因为我基本都在错峰出行,很多地方的人流量在工作日会少很多,包场看电影是很常见的。我们还可以做地理套利,比如去生活成本更小的地方生活。我看到公司有个数字游民计划,就是几个同事一起约好去一些未曾待过的城市和地方边工作边旅行,真是很羡慕这样的自由生活,可是我有两个小孩需要照顾😂。

总体而言,我已经习惯了远程办公,好像就再也回不去坐办公室的日子了,这是适合我的一种工作方式。

升级我的 localhost

作者 yukang
2023年5月31日 07:17

因为开始了全职的远程办公,所以我想把自己的 localhost 打造得舒适一些,最近一直在断断续续升级工作相关的设备,对于整天生活在屏幕前的数字宅来说这犹如买新房和装修吧。

我对设备的要求并不是很高,但年级大了多少有点职业病,所以我打算这次尽量找些好设备来满足自己,主题就是“关爱中年程序员”。


Mac 及应用

我近两年都在使用 PC 笔记本当主力开发机,最近几个月重回了 Mac 的怀抱。即使苹果生态有我之前提到过的各种问题,但 M2 芯片的 Mac 性能和续航我都很满意。不过我也花了好些时间来找称手的工具和配置。

我买的 Mac 是 32 G + 12 Core + 1 TB 的中等配置,这个配置完全满足我日常需求,大概是因为我还没什么视频剪辑类的事要做,我的主要需求是编程、写文档、浏览网页这些普通事项,一周需要带出去两三次,因为我又不玩游戏,所以 Mac 还是挺适合我的。

14 寸的 Mac 外出办公稍微有点重,如果你已经有适合外出携带的笔记本,在家里放个 Mac Mini 也是很适合的选择,因为 Mac Mini 价格太美丽了,而且接口更丰富。新款的 Mac Air 看起来也很不错,也许 M3 是个入手的好时机。

我用了两个来月才完全重新适应 Mac,下面谈谈一些配置和应用:

首先系统默认语言使用英文,这样有个好处在于很多配置都可以用字母去搜索,应用的切换也完全不用中文,减少了切换输入法的动作。

我形成了一个使用习惯,那就是严重依赖各种 App 的 Command Palette,比如 VsCode 我把 Ctrl+L 绑定到 command palette,然后 Obsidian 和 Arc 也同样有 Command bar。使用 Command Palette 通过模糊搜索去跑命令是更统一的方式,因为快捷键太多了我根本记不住,而命令是一个个普通的英语单词,容易在心里念出来。

另外我会关闭所桌面和应用切换动画,这样操作起来会迅速很多。

Contexts

Mac 的应用切换简直惨不忍睹,而且这些年来都没有很好的改进。市面上有不少这类工具,足以看出很多人不适应系统原生的应用切换。特别是像我一样会用 VsCode 为不同的项目打开多个窗口,快速定位到窗口对我来说是一大刚需。

Contexts 这个应用是我想象中工具,完全贴合我的使用习惯和对细节的要求。安装后 Cmd + Tab 就替换了系统原有的应用切换,这是符合大多数人使用习惯的 Windows 风格的切换,另外我会把一个很重要的快捷键 Ctrl+I 绑定到 Contexts 里面的 search,这样我能快速通过部分关键词选中要跳转的应用。还有,Contexts 很贴心地有个自动学习用户使用习惯的 Number-Switcher,基本上我日常 右Cmd+W 跳转到 Wechat,右Cmd+D 跳转到 Discord 等等,真是提高效率的利器。

另外很多人推荐 Raycast,这款软件可以做很多自动化的功能,但目前我只是用 Raycast 来查找和启动应用。

Arc

我使用 Arc 接近三个月了,目前已经成为了我的默认浏览器。我认为最有用的是 command bar,可以输入 command,extension command,tab url,我把 Ctrl + L 配置到 command bar,这样我基本不会去关注 tabs 了,也不会去收藏网页,一切都是用关键词搜索。

其中的 Space 功能我也很喜欢,比如公司用 Gmail,而自己也用私人 Gmail 账户,Space 就可以把不同场景的同样网站区分开来。

Chrome 的所有 Extension 在 Arc 上都可以使用,而且我也不会再为插件配置快捷键,常用的命令通过 command bar 触发。

Easel 是 Arc 的另外一个好用功能,我们可以很直观地通过类似截图的动作就能组合成一个 Dashboard,比如我把邮件、Github PR、Meeting Schedule 组合成一个看板,能够一目了然地看到需要关注的信息。

Input Source Pro

这个工具用来设置一些应用的默认输入法,为什么这个很重要呢?因为我特别烦切换输入法,这是一个很干扰心流的动作。

比如在 Terminal、Raycast、Contexts、VSCode 这些应用里面,95% 以上的概率我都只会用英文输入,所以配置这些应用的默认输入法就能很大程度上减少切换。

这类工具有好几个,我用得最舒服的就是 Input Source Pro,这个工具还在 Beta 阶段,目前免费。

Karabiner

这是修改快捷键必备工具,我做了一些方向键的配置,另外配置 Ctrl 的键,因为这比 Cmd 好按:

- Change left_command+hjkl to arrow keys- Change left_command+u/i to page_up/page_down- Ctrl+Z => Cmd+Z (Undo)- Ctrl+T => Cmd+T (New tab)- Ctrl+W => Cmd+W (Close)- Ctrl+S => Cmd+S (Save)

不管是 PC 还是 Mac,我必须做的配置是把 Caps Lock 映射为 Ctrl,因为我们很少使用 Caps Lock,而这个键位是非常适合小拇指去按的,如果你是键盘党,这样配置可以很大程度减少左小拇指的损伤。

Rime 输入法

大半年前我改变自己的使用习惯,强制自己使用双拼输入,目前我已经完全适应,总体而言我认为双拼没有极大地提高我的输入效率,但确实减少了很多不必要的按键,另外我觉得敲字的节奏感会好些。

我之前看到很多人吹小狼毫,但几次打开那些文档我都没有折腾的欲望,大概是我对输入法也没有特殊的需求,系统默认或者搜狗之类的都行。

两个月前偶然在 Twitter 上看到一个人推荐 Rime 雾凇拼音,这次我试了试。虽最后还是花了点时间折腾,但我觉得这个投入是值得的。Rime 的输入体验是好过 Mac 原生的输入法,没有任何多余、花哨的功能,你不输入时不会感觉到它的存在,并且支持我喜欢的小鹤双拼。但这东西就是注定比较小众,配置个输入法需要用到很多 Yaml 文件估计会劝退很多人。

我另一个比较特别的配置是,不像大多数人那样通过按 Shift 来进行中英文切换,因为 Shift 是一个常用键,容易误切换输入法,另外一个原因是我希望有一个唯一的标识来识别目前的中英文状态,而 ShowEdge 就是这样一个工具,我在屏幕特定的边沿会配置一个小圆圈,如果是红色便是中文,如果是黑色便是英文。这样不会存在我开着 Rime 输入法,但是输入的是英文的情况。我使用 Ctrl+J 或者 Caps Lock 来切换输入法,因为 J 是右手最容易默认找到的按键。

键盘和鼠标

我以前也主要看重键盘的外貌和敲打手感,所以我买了个 HHKB,后来也买了宁芝等各种小尺寸的键盘。最近两年我越发觉得肩胛骨酸疼,特别是右肩胛骨,有时候晚上疼得我睡不好。

我稍微调查了一翻,感觉确实是因为自己长期的坐姿和使用键盘的习惯造成的。Mac 的键盘和各种小尺寸键盘,因为宽度不够,两个手都要往中间靠,这样肩胛骨就长期保持这个姿势容易出问题:

人体工程学这东西有的人说是智商税,但我还是试试吧,于是我购买了套罗技的人体工程学鼠标键盘

这个罗技 K860 尺寸巨大,其固定的手托材质舒适,这键盘需要大概一周左右的时间去完全适应,毕竟很多人的指法也是不对的,这种双手分离式键盘需要大致正确的敲打指法。

唯一的缺点是数字小键盘,毕竟我们大部分人是不用数字小键盘的,这有点浪费空间。刚用的时候我觉得很难受,我需要伸长手去摸我的鼠标,所以很想退货。但巧的是我买的是个套装,里面还有个轨迹球鼠标。当我把这两个设备这样组合起来时,小键盘的问题解决了:

使用轨迹球也是个神奇的体验,刚开始觉得操作起来太慢,适应了之后觉得右手轻松不少,反正只需要移动大拇指就行。同时鼠标也可以使用键盘的手垫,而且如果移动键盘鼠标也可以跟着移动了,配合 left_command+hjkl to arrow keys 方向键那里我也不会去按的。

很多程序员不喜欢用鼠标,但我最近发现鼠标的前进和后退键其实非常有用,比如我看代码的时候,按住 cmd 键点击鼠标可以跳转到定义,然后按鼠标的后退键返回之前的位置,这比一直使用键盘会舒服很多。

Mac 的 Trackpad 手感和体验都是很好的,但因为 Trackpad 也是居中的,和小尺寸键盘同样用久了同样容易劳损

如果你用外接鼠标,还需要另一个小 App 来配置一下。因为我们通常适应了 Mac 默认的 Natural scrolling,但是鼠标用这个选项就会很诡异,所以我找到了 Scroll reverser 这个工具,可以单独设置滚动的控制方向:

我对这套设备非常满意,肩胛骨酸疼这个问题很大程度上得到了缓解。现在我偶尔外出使用原始的 Macbook 键盘和 Trackpad 时,我会极其难受,很难想象自己使用这个姿势这么多年,不出问题才怪。

但这临时的缓解也可能是只是因为换了姿势,彻底解决长期的问题,大概只能尽量少坐多运动,不要长期保持同样的姿势太久。

Herman miller

我之前的椅子扶手快坏了,然后想着买个新的办公椅。按照这次升级的主题,直接选择了购买 Herman Miller 这把号称世界上最舒服的椅子。

我之前偶尔试过同事的这款椅子,确实很舒服,但我自己买的刚开始坐上去觉得有些偏硬,后来找了些视频资料发现是我的坐姿有点问题,这椅子适合正坐而不是半葛优躺那种坐姿,用了几周后确实能感受到差异了。二代的前倾功能很适合专注的时候使用,而这个功能在目前的椅子上相对少见。

由奢入俭难,再也回不去了,如果这把椅子帮我纠正了坐姿也算是值了:

其他

这期间我还新增了些其他设备,比如 4k 显示器、支架等等,加上多年前买的升降桌等就完全够用了。我推荐的另一个小设备是韶音的骨传导耳机,我经常带这个耳机半天一天的都不觉得难受,而且这个品牌的质量和售后我非常满意。

最近我还看了看桌面的布线之类的东西,毕竟作为数字宅男是无法拒绝一个类似这样的桌面美学:

什么东西但凡涉及到美学就会是个无底洞,然后我开始怀疑自己是不是要掉进另外一个坑,需要及时止住了,所以我买了个隐藏电线的盒子把桌面搞干净点就够了。

总之,我对现在这套办公设备非常满意,程序员要对自己的身体好一点,毕竟这些东西差不多占据了日常的大部分时间,所以值得投入些时间和金钱在上面。

如果你有什么好用的设备请推荐给我,虽然我现在也不一定会买,但我喜欢种草 😜

学习英语的新工具

作者 yukang
2023年5月23日 19:10

我之前写过文章强调英语的重要性,这些年我也一直还在注重提高英语能力。虽然我在外企工作了两年多,但其实日常表达中使用口语的时间不多,所以口语提升有限。

最近我发现了些新的学习英语的好工具,顺便分享一下自己的一些心得。

Discord 英语小组

Discord 是我新工作的日常即时沟通工具,Discord 的服务器上有很多英语练习小组,我习惯的是这个 ,在这里可以和全世界各个地方的语言学习者聊天,有三四人以上的房间,也可以两个人进行一对一聊。

这个方法是和一个读者交流时他告诉我的,他在短时间内提高了口语最后面试成功。我使用 Discord 练习口语已经有一个半月了,我觉得效果非常好。

可能是因为时区原因,里面大部分是亚洲和中欧国家的人,比如印度尼西亚、印度、泰国、土耳其、俄罗斯等,据我统计大学生居多,也有不少高中生。这些人虽然不是英语母语者,但大部分比中国普通的英语学习者要好。

你在 B 站可能也看到过一些人用 OME TV 之类的软件来练习英语,但如果你自己试过就知道这不适合练习英语,因为总会碰到奇奇怪怪的人,我就碰到过一个恶作剧少年,聊着聊着突然给我来了个恐怖的鬼脸,还有会碰到色情等。所以想在上面找到合适的人时间成本太大,而且也很难停下来进行一些较为深入的话题。

我之前也付费在 Cambly 上提高口语,但我现在觉得那种方式太正式,用 Discord 比较随性,默认不用看到对方,所有注意力集中在听和说上面,也没有犯错的心理压力,适合我这种有些社恐的人。有时候我一边爬山一边和人聊,目前能比较流畅地用英语沟通很多方面了。我们聊的大多是一些日常的话题,如果碰到些计算机相关的从业者和学生,也可以聊聊技术方面的事。

另一个好处是能够与来自不同国家和文化背景的人交流,从而获得不同的感受,更深入地了解这个世界。除了一些常见的国家,我还与来自蒙古、埃及、巴基斯坦、孟加拉国和也门这些小众国家的人聊过。

我印象比较深的是一个也门的初中生,她说她不能上学了,因为自己的国家在内战。我不知道也门这个国家原来还如此动乱,回来后通过搜索去了解一些背景。我还碰到过一个埃及人,他的工作是客服,他想要学习英语因为英语客服的工资更高,因为他有残疾几乎一个多月没怎么出门了,那天我们聊了很久。我碰到过俄罗斯年轻人,他说现在非常恨自己的政府,正在想一切办法逃离俄罗斯。还有碰到过一些纯粹的语言爱好者,他们能流利地说几种语言,模仿能力极强。

有的人聊得比较投机就会加个好友,以便保持联系。因为同是语言学习者,所以大部分人都比较有耐心,有时候表达不清楚了,有的人也会共享屏幕给人解释。如果在屏幕前,我喜欢打开 Google 实景,和对方聊聊他们生活的国家和城市。也许是我太久没出去了,对 Google 实景比较着迷。

我和大部分中国学生一样,即使英语的读写能力还行,但是口语一直很薄弱。语言这东西就是个日常技能,如果你掌握了基本的语法,就没那么条条框框,即使是发音不标准也不是什么大问题,正常和流畅的表达是需要很多自信心的,而如果没有练习就不会得到这种自信,如果你练习得足够多,在表达的时候就没有那个在脑子里翻译的过程。

Building Your English Brain这个课程很好,构建自信心的过程在于你能把简单的单词完全掌握,并且熟练使用,造句就是一个很好的锻炼方式,而和人聊天就是最自然的方式。

也许我的口音很难有大程度的提高了,但表达的自信和流畅度是相对容易提高的。

Trancy 插件

Youtube 是最好的英语资料库,这里面可以找到很多不同层次的视频资料。 Trancy for Chrome | Master a new language in an enjoyable way 这个插件可以让你进入几种不同模式去学习英语,比如跟读、单词考查等等。

这是最近让我眼前一亮的插件,特别适合用影子跟读法去用 Youtube 的资料来锻炼口语:

这个插件还在不断更新中,现在我们可以模拟很多场景,跟读并且获得评分,如果有不认识的单词还会自动给标注出来。

沉浸式翻译插件

这种插件 immersive-translate 适合看一些比较长的英文文章,我有时候也用来看 NewsHacker 之类的。但我不推荐一直打开这个插件,而只是在想要快速浏览文章获取信息的时候试试。因为如果你一直依赖这种翻译,就不容易培养出来阅读英文的习惯。

各种 GPT 及 AI 工具

ChatGPT 的翻译水平超过大部分翻译工具,我现在写稍长一些的英文都会让 ChatGPT 帮我润色一下。

之前我还试用过 AI 聊天软件 myshell,不过自从我用 Discord 和真人聊之后就觉得 AI 工具没什么意思了,还是和鲜活的人类聊天更有趣,即使人的发音没有 AI 那么清晰和流畅,但那种人与人之间的情绪感受和生活经历的分享,是 AI 所不能替代的。


学习一门语言犹如打开一个世界,而发现一种新的学习方式也犹如打开一个世界。如果你有什么好的学习方式和工具,欢迎和我分享 🙌。

新的旅程

作者 yukang
2023年5月12日 06:40

因为家庭原因,我决定回到深圳常住。2023 元旦前夕,我陆续把东西寄回深圳,元旦后我在晨曦中登上了往南的列车。我那时候还没确定会离职,但目前找到了合适的工作,所以就于四月份从微软离职了。

在苏州微软工作的两年多,我遇到了很好的团队和同事,真的要离开了也有些不舍得。微软很大,每个部门的工作氛围可能都会有些差别。苏州工作的两年间我学到的这些:

技术,世界上很少有公司能用技术服务这么大体量和规模的企业用户,我记得上次看过一个详细的事故报告,写着影响的用户上十亿,写出这样一个 Bug 也是个难得体验了。微软的工作氛围很好,作为技术人待在里面很舒服。在一个大的组织里工作很多时间都是在沟通,如何推进项目,如何和同事达成共识,这些是非常重要的也是最难的。另一方面,在长达 20 年历史的项目上做维护和开发也是一份难得的经历,我入职那几个月基本都在看代码,极大地提高了我的在编程上的韧性。另一方面,在一个如此大的项目上工作总感觉是在戴着镣铐跳舞,大范围地修改代码是很难的,很多时候都顾不上代码的好坏,一切都以稳定性为重。

包容,这微软的企业文化。公司里有各种背景的人,而 Leader 大多会遵循员工自己的一些想法。如果一个员工犯错了,很可能是组织和流程的问题,而不是这个人的问题,所以在做事故分析的时候,都在就事论事。也许也是因为自己年纪越来越大,我对很多人和事都更包容和平淡了。

生活,苏州的生活很舒服,感觉比较清淡,晚上八九点街道上的人就比较少了。我住在苏州的工业园区,人口密度相比深圳小很多。周末经常去湖边和公园玩耍,在这两年间我开车去周围都逛了逛,我喜欢去太湖、独墅湖、阳澄湖、诚品书店这些地方,风景都很好。工作之外认识的人不多,但也有舍不得的朋友。有一次我在金鸡湖旁边看到一家三口在夕阳下露餐,他家的女孩和我大女儿差不多大的样子,养着两只小鸭子作为宠物。我正被这幅和谐画面吸引,没想到我女儿也被两只鸭子吸引过去了,然后我们一起散步玩耍并留下了联系方式,周末经常约着出来遛娃和闲聊。这两年间我的社交圈更小,但留给自己的时间多了,找一些自己想做的事情。平时我花了很多时间在二女儿的照顾上,和孩子玩耍是是一种最好的休息。

所以,这两年不管从生活还是工作的角度我都是满意的。2020 年我攒了很大的勇气离开工作了六年的公司和城市,如今我又花了很大的勇气改变。其中很重要的一点还是父母方面的考虑,我在另外一个城市待的这两年,和父母相见的次数不足十次。The Tail End 这篇文章用残酷和直观的数据让人认同这点:Living in the same place as the people you love matters.

抛去一些不能改变的家庭原因,我还想要什么其他改变?从另一个角度看待,人生是一个游戏,不能暂停,不能回滚,但必然有终点。在这些基本限制下,有的人会选择一直玩一种剧本,而有的人会选择多玩一些剧本。在我的履历中,我认为年轻的时候探索和尝试还不够,所以现在想趁还没老到不能折腾阶段,想改变就想试试,我还有好奇心。

另外关于如何过好这一生,对我有启发的观点是李自然的如何把一辈子活成 N 辈子,这个视频值得看看。其中很重要的是需要做减法,走出舒适区,保持提高。这倒不是说要赚多少钱,而是既然我们大概无法控制长度,那我们如何从厚度去扩展。

成功就是不断地达到自己理想的生活状态,我的理想的生活状态是更自由,并且能有足够多的时间和精力陪家人,做技术和开源,所以全职远程是更适合自己。当我觉得未来可能离开微软的时候,就是抱着这样的期待开始留意一些工作机会。

可刚回深圳的那段时间,因为一些突发问题家人住院,所以这几个月我非常忙。父母年迈生病,孩子幼小,任何人到了这个阶段都不太容易吧。印象深刻的一次是我开车去给家人送饭,但因为实在太累,等红绿灯的时候想着迷眼一会儿,结果我居然睡着了,隔壁的司机下来敲我的车窗才突然惊醒,估计我就睡着了几十秒,但醒来后像是睡着了好久。

这阶段比较辛苦,幸好现阶段结果还好,深圳的医疗虽然比不上北上广之类的城市,但也有些资深的好医生在多地的跑,在常见的疾病上是能找到靠谱的医生。

另外今年工作机会不多,在我上面所列的期望条件下就更少了。我之前收集过一个远程工作列表,很多公司今年基本不再招人。幸运的是我找到了一份自己挺满意的工作,就是在 Cryptape 做区块链和 Rust 相关的开发。我知道区块链这行现在鱼龙混杂,很多人现在也并不看好,我认为这里面有很多新东西可以尝试。秘猿也是国内很少的那种企业,在做一些新事物的探索,同时给了员工很大的自由度,公司里几乎全员远程工作,所有代码也都是开源的,通过我入职后的一个月感受来说内部氛围很好。

这次我花了比较久时间来做决定,特别是家人不太理解我人到中年为什么就变得更激进起来。作为大龄技术人,在国内换个靠谱的工作并不容易,但我还没到退休的年龄。我没那么大动力在一个公司里按照既定的职级路线去爬,但还有动力去写代码,提高自己的编程能力,在开源的世界里进步。

微软是个令人尊敬的良心企业,这个有接近 50 年历史的企业再一次抓住了历史的风口。在和组里告别的时候,我说现在也许不是一个离开公司的好时机,这感觉就像是火箭正要发射而我却要下来。若干年后,再回顾起来看我的选择,是不是会显得我很愚蠢地偏离了大势。有可能,但更重要的是人要自洽,每个阶段追求自己理想中的生活就很好,我现在的生活状态和预期的几乎完全一样。

这段时间两鬓多了一些白发,觉得时间越过越快,所幸还未被生活击溃,我所遇到的困境并不特殊,而是大多数人这辈子都会面对的。

以上就是这半年里的一些改变的总结吧。

不想当作家的程序员写不出 Redis

作者 yukang
2023年4月5日 01:59

西西里岛,是位于意大利南部阳光而宁静的岛屿,正如电影《西西里岛的美丽传说》中演绎的那样,这里有着古老的历史和建筑,看起来和 IT 不沾边,却是 Redis 的作者 antirez 的居住地。

Redis 是互联网的一个基础设施,这个世界上大量的网站背后都有 Redis 的影子。相比于 Redis 的流行度,很多程序员并不了解 antirez 的故事。最近我看了他几乎所有的博文,和你分享一下我了解到的趣事以及我们能从这位 70 后的上一代程序员身上学到什么。

antirez 不是一个典型的意大利程序员,大部分当地人喜欢 boring-but-sure 的路径,IT 这行在当地算不得特别高薪的工作,大多数人不喜欢做过多探索,但 antirez 喜欢新东西和创造新东西。在一篇 10 年前的访谈中 antirez 谈到,住在意大利对程序员而言没有多大的影响,因为我们可以在互联网上经历有趣的一切:

If your target is the world, being here is not a big limit for a programmer. The majority of interesting things are happening on the internet nowadays anyway.

职业生涯初期,antirez 做过安全研究员,后来做过嵌入式、系统、Web 等领域的开发,他早年还发明了 Nmap 中的常用的扫描技术 idle scan

2009 年,当时 antirez 在做一个网站实时统计,他认为现有数据库比如 MySQL 无法满足那种写入密集、查统计数据的需求,于是他着手解决这个问题。

antirez 使用 Tcl 快速撸了一个名为 LLOOGG Memory DB 的模块,总共只有 300 行,但却解决了手里的问题并包含了 Redis 的核心设计,有 protocollist,还有 6379 端口!

随后 antirez 重新用 C 语言实现了新的版本,投入生产环境良好运行了几周,随后发表在 Hacker News 上并由此得到了更多关注,越来越多的公司开始在生产环境运行 Redis。

这里谈点题外话,Redis 最初在 Ruby 社区受到了关注,随后 Github、Instagram 等站点开始使用 Redis。Ruby 社区曾经是潮流的引领者,比如 Git 最初发表 Linus 也搞不懂为什么 Ruby 社区的人们这么喜欢这东西,后来就有了 Rails 写的 Github。虽然 Ruby 现在略显式微,但我们应该感谢 Ruby 社区的好品味给业界发现和创造了这么多好东西。

如果只是用来做缓存,2003 年开始我们已经有了 memcached,比如我 2011 年刚工作那会儿 memcached 是更成熟通用的组件,我还仔细读过 memcached 的源码,但为什么 Redis 能后来居上?

antirez 谈到主要有两点:

  • 对于密集写入的场景,特别是缓存相关的需求,Redis 可以节省成本,性能也很好
  • Redis 不只是缓存,而是一种不同形态的数据库,适合很多性能比正确性要求更高的场景

In the field of programming languages there is a motto: a programming language is worth learning if it is different enough from all you already know to change your mind, exposing you to new abstractions. Well I think Redis definitely is a really different database, and will change the way you think about your data.

antirez 从一开始就把 Redis 当作一个数据库来看待,而不只是缓存组件,简而言之 memcached 能做的 Redis 也行,而 Redis 能做到的 memcached 不行。

Redis 天然支持各种常用的数据结构,比如 list、set、maps 等等,这些数据结构让 Redis 可以应对各种业务需求,可以说 Redis 开启了一个 KV 数据库的新时代。

这个故事和 SQLite 的由来相得益彰,SQLite 的第一个版本只是个 Tcl 扩展,Git 的第一个版本只有 1200 行,这似乎印证了软件设计中的一个道理:运行良好的复杂系统往往由简单设计演化而来,而一个从开头设计的复杂系统往往不行,通过打补丁的方式通常也无法解决:

A complex system that works is invariably found to have evolved from a simple system that worked. A complex system designed from scratch never works and cannot be patched up to make it work. You have to start over with a working simple system.” – John Gall

Redis 和 SQLite 都深受 Tcl 的影响,而 Tcl 是一门 1988 年发明有着 35 年历史的语言。Tcl 继承着 Unix 那种设计可组合、小巧的组件并保持简洁接口的设计风格,antirez 认为自己的程序理念深受 Tcl 的启发,特别是 Tcl 里 All data types can be manipulated as strings 和命令的风格延续到了 Redis 里。而 John Ousterhout 当年是为了解决 EDA 开发 中的一个需求发明了 Tcl,在互联网浪潮还未掀起的 80、90 年代里 EDA 真算是个技术领域里的一颗明珠,衍生出了很多技术分支。

Redis 的源码可作为 C 系统编程的典范,还包含了很多经典的数据结构的实现,你可以读读这本 Redis 设计与实现 来领略一番。

随后 Redis 被 VMWare 赞助,后来又成立了专门的 Redis Labs。虽然 Redis 从一开始就是个热门的开源项目,但我们可以从提交数排名可以看出,在这近 10 年间主要是 antirez 一个人在做贡献和维护。

这是一个较为奇怪的现象,毕竟 Redis 是一个如此通用的组件,Github 上的关注度也很高,来自各种场景的需求会非常多。

但如果你仔细回顾 antirez 的风格,这就有了合理的解释。他认为对 Redis 来说,避免复杂度、保持稳定性是最重要的事,所以每天他面对众多的需求和 PR 时,大多数时候他都会说“No”,否则就会出现 Less stable code base, more problems 的恶性循环。

The bugs you write in the first implementation are extremely hard to fix later. They don’t go away easily. Basically there’s this process where you say, okay, I want to change something, but I want the software to remain stable. So you start to think about it for weeks, the way you want to do it, without writing any code.

每个新功能的引入必须要非常小心,Redis 的核心代码一直保持在万行的级别,并且在 4.0 版本之前都是单线程运行。

设计才是最重要的,而编码和实现是简单的,因为这只是水到渠成的事

Instead, there’s this huge design process. But because of this design, sometimes we can write a new feature using half as many lines of code in a much more simple way, a much more stable way. You think and think and think and find that a couple of days ago, what sounded like the best design — it starts to sound pretty lame, actually, and you find another and another. At the end, you understand that probably that specific one was the best, and then you start doing the implementation.

This means, in turn, that people say, don’t worry, I can help you implement this. And you say, no. If you want to help me, you have to put more time into the design effort. Writing the code is the easy task. The hard task is understanding what to do and in what way to do it.

我想,这也是我们作为程序员想要提升到更高层次必须意识到的一个方面,不要过多花时间关注在编码上,而应该是花时间在思考需求和问题、找到好的设计这些事情上

长久维护一个项目也会感觉到无聊,为了让自己保持兴趣,antirez 会尝试在不同的领域切换,比如一段时间做数据结构,过几周就切换到 cluster 之类的。并且除了 Redis 之外,他还在做更多 side project:

1) Load81, children programming environment.
2) Dump1090, software defined radio ADS-B decoder.
3) A Javascript ray tracer.
4) lua-cmsgpack, C implementation of msgpack for Lua.
5) linenoise line editing library. Used in Redis, but well, was not our top priority.
6) lamernews, Redis-based HN clone.
7) Gitan, a small Git web interface.
8) shapeme, images evolver using simulated annealing.
9) Disque, a distributed queue (work in progress right now).

作为程序员 side project 是一种探索也是一种精神休息的方式,并且程序员通常会在 side project 中显得更有创造力:

Like a writer will do her best when writing that novel that, maybe, nobody will pay a single cent for, and not when doing copywriting work for a well known company, programmers are likely to spend more energies in their open source side projects than during office hours, while writing another piece of a project they feel stupid, boring, pointless.

我粗看了一下这些项目,都是 C 实现的并且风格统一,README 都会写些自己的思考和设计选择。

antirez 喜欢写小的程序,这不只是因为可以控制复杂度,而是因为短小的程序自有其美 Fascinating little programs,老一代程序员喜欢在严苛的限制下挑战自己的技艺,比如 Writing an editor in less than 1000 lines of code, just for fun

独立维护一个影响如此大的开源项目会有巨大的心理压力,虽然用户并没有直接付钱,但维护者有责任去修复出现的问题。这种压力不一定是技术上的,也有来自社会方面的压力,比如不断有人指责他不把代码和 API 里的 master/slaver 替换掉。

antirez 曾经在一篇采访中谈到想在家里组成一只小队伍来维护 Redis,后来又感叹到现在太难找靠谱的 C 程序员了,愿意做 system programming 的越来越少。

antirez 在这篇文章中 The struggles of an open source maintainer 阐述了维护开源项目的难处,其中提到在 Redis 项目用户多了之后,自己需要一直处于在线状态。而他习惯的工作方式是工作一段时间然后彻底放空,他从来不习惯朝九晚五的定时工作制,甚至提到自己无法保持编程 40 分钟以上,他喜欢编码一会儿然后去带孩子或者运动一会儿再回来。

除了编程之外,antirez 对红酒和运动也很有兴趣,而他更有追求的是在写作这件事情上,可以为了写作放弃编程,这让我想到了王小波。

三年前,他发了一篇博文声明自己从 Redis 上退下来,因为他认为自己想做的是艺术家那样的创造性工作,编程也是自己表达方式的一种,而 Redis 发展到现在这个程度创意性事情更少,事务性的工作更多了,这不是自己所期望的:

I write code in order to express myself, and I consider what I code an artifact, rather than just something useful to get things done. I would say that what I write is useful just as a side effect, but my first goal is to make something that is, in some way, beautiful. In essence, I would rather be remembered as a bad artist than a good programmer.

Redis 交给了几个核心维护者,这些人已经和他在开源社区有多年的配合,所以对社区来说这是个很自然的选择。

在停下编程的这几年,他默默地完成了名为 Wohpe 的科幻小说,这本小说是关于人工智能和气候变化等,原文用意大利语写成,但现在也有英文翻译版本。

I now know for sure: it is no coincidence that for hundreds of years writing has been considered the highest art in which to try one’s hand. By writing you look for things, and if you insist enough you end up really finding them.

写完小说后 antirez 还有些犹豫到底是回到编程还是继续从事其他写作,或者是边写作一边做些技术。直到前段时间,暂别了两年之后 antirez 终于通过 Advent of Code 找回了编程的乐趣,他解决了 18 道题目,并且在过程中又折腾出来一门 stack-based 的编程语言。从 Twitter 上看,最近他在做一些 LoRa 设备上的小项目,也折腾 Flipper Zero 这样的极客设备。

总之,antirez 的博客非常值得一读,其中有一篇 英语是我 15 的伤痛 让我很有共鸣,作为英语非母语的开发者,要融入到英语的环境中需要很多额外努力,而这是技术圈里大多数英文母语者根本不会在乎和谈论到的事。

他的博客里还有些好文章:

  • 编程中保持心流,编程中如果有新的想法和问题发现,你可以先记录下来以后再回顾,这叫作 Log driven programming
  • For me other people making money out of something I wrote is not something that I lost, it is something that I gained. Redis 使用 BSD
  • Arts are one of the few things worth life’s best efforts. Programming is art, if done in certain ways.
  • Life is too short to work like crazy for most of its part.
  • The mythical 10x programmer
  • Programming and Writing

看了这么多 antirez 相关的文章后,最让我印象深刻的也是好奇心,并且在好奇心的驱使下不断去做有乐趣的事。

保持兴趣,不只是技术上,生活上的兴趣也很重要,红酒、CrossFit、写作,以及冰激凌,antirez 和 Redis 的第一位用户 (老同事) 一直合开着一个冰激凌店!

人类的终极工具

作者 yukang
2023年3月20日 08:19

乔布斯曾经说过:计算机就像自行车一样,它们是人类思维的自行车。计算机和自行车一样,能够帮助人们快速、高效地完成很多任务,是人类思维工具的一种。

随着 chatGPT 的出现,计算机这个工具彻底地进化了,它比人类懂得更多,能通过语言和人交流,懂得推理和归纳,帮你学习、创造,计算机也可以成为”人类思维的朋友“。

LLM 技术的破圈夹杂着兴奋和恐惧,很多行业都面临巨大的变化。

很明显的是 Google 危了。以我最近的使用感受来说,使用 Google 搜索的次数会越来越少,我只有在明确想查找些网页的时候才会去用 Google,而关于一个细节、主题的搜索和学习,我会先尝试用 chatGPT。

以我最近碰到的一个例子来说,我碰到一个单元测试用例跑失败了,从错误信息看是“conflicting output format options”,当我把这个场景描述出来后,chatGPT 给出了非常好的回复,而且我们可以通过继续追问的方式来学习更多:

当然也能通过 Google 来通过关键词搜索,但我知道这样会更麻烦,我需要从一堆沙子中去寻找有价值的东西,而且我不能和搜索引擎对话。

用 chatGPT 非常适合苏格拉底式的对话和探索式学习,比如我想了解一个主题:

接着我继续问:

  • what is bound lifetimes in Rust
  • show me some example code for Rust’s generic lifetime parameter
  • show me some example code for Rust’s higher-ranked lifetime
  • show me some example code for Rust’s higher-ranked lifetime in a trait
  • show me some example code for Rust’s generic lifetime parameter in a struct
  • show me some example code for Rust’s generic lifetime parameter mixed with high-rank lifetime
  • In Rust, can ConstGeneric work with lifetime parameter?

chatGPT 给出的回答都非常好,能让我就想关主题不断地探索。当然 GPT-4 就更恐怖,我一个朋友最近在上经济学的课程,每周的 quiz 基本都被 GPT 给秒杀。

我最近写了个几十行的 Python 小程序 chenyukang/talkGPT,通过 SpeechRecognition 和 OpenAI 的接口来进行英语口语对话,可以基本运行成功 (OpenAI 接口有时候比较慢)。后来我发现了其他公司做的这类产品 Telegram: Contact @samantha_x64_bot,已经能非常流畅地和黑寡妇进行口语练习了:

所以,chatGPT 完全是另外一个维度的工具,积累了人类历史上的很多文字和知识,并且可以不断地进化。如果具备一些人类的核心能力,比如逻辑、推理和归纳,chatGPT 就是一个活了几百年的人,并且随着时间的推移不断地进化和自我更新。当 chatGPT 这种技术和波士顿动力的机器人结合起来,这个活了几百年的老人就有了身体。这是不是很恐怖?

当人类真的创造出来这样的工具,我们该如何改变自己的学习和工作方式?以后每个人都可以通过移动设备和这各种 GPT 交流,几乎所有的创作中机器生成的成分会越来越大。

这是巨变的开始,很多问题现在没有明确的答案:

学生在学校该学什么?也许所有背诵的东西都更不重要了,学生应该学习的是更多通识教育,如何自主学习和创新。

编程和写 Prompt 有本质的区别么?我以前认为有差别,但如果你把 chatGPT 看作一个编译器或者解释器,其实也没有多大差别。只是编程更为精细,这是更直接和计算机对话的原始方式。而 Prompt 几乎就是自然语言,你可以通过特定的 Prompt 完成特定领域的任务,Prompt Engineer 的需求确实会存在。现在我们常用的编程语言对于未来而言可能是一种汇编语言。

通过 chatGPT 创造和辅助创造的作品是否有版权?我不知道,似乎法律还没跟上这块。以后必然会出现大量的机器生产的内容,纯手工打造会变得稀缺,但如何区分出来是否纯手工打造,或者这将变得不重要。事实上这篇文章里就有部分 chatGPT 帮我生成的 😁。

书籍会变得更不重要?人们会更没耐心去从头到尾地看书,也许我们可以通过发布思维和知识库这样的东西来分发知识了,比如刘润把自己的所有书籍和文章汇总成为一个主题知识库,我们可以与之对话,比如有人汇总了 Paul Graham GPT

我们如何面对这样的新工具?最重要还是把它定位为工具,用来提高效率和创造价值,正如 John Carmack 在回答是否担心 AI 替代程序员时所说:

Software is just a tool to help accomplish something for people - many programmers never understood that. Keep your eyes on the delivered value, and don’t over focus on the specifics of the tools.

赶紧学起来?

Rust 编译器源码概要

作者 yukang
2023年3月13日 08:24

一个 Rust 程序是如何从源文件编译为二进制文件的?

如果从头开始看 rustc 的源码会无从下手,我之前通过解决 issue 去读过部分模块的源码,就是 bottom-up 的方式,但我还未从整体上理解 rustc 的源码结构。

这篇文章主要是我在重看 Rust Compiler Development Guide 的一些随手记录,还有些自己的动手实验,旨在厘清编译器的大致脉络,理解每个阶段做了些什么,如果你想看更为完整的文档请参考官方的手册。

Rust 编译器分为这几个主要的阶段,回顾我目前做的工作大多集中在 MIR 之前,分阶段从前到后接触得越少 😊

整体架构

编译器是典型的输入输出系统,每个阶段都有对应的入和出。我们经常可以看到 lowering 这个术语,这个 lowering 的对象可以是源程序、AST、IR,不断地把程序中的抽象由高变低的过程,到 MIR 就已经是类似 LLVM IR 这个级别了。

Lexing: 把源程序解析为 token 流。

Parsing: 把 token 流转换为 AST(Abstract Syntax Tree),这期间很做宏扩展、AST 验证、名称解析和早期 linting。

HIR lowering: 将 AST 转换为高级中间表示 HIR(High-level IR),这是一种对编译器更友好的 AST 表示,其中也涉及很多诸如循环和 async fn 之类的脱糖。然后我们使用 HIR 进行类型推断(type inference)、特征求解(trait solving)和类型检查(type checking)。

MIR lowering: 将 HIR 转换到 MIR(Middle-level IR),用于借用检查和其他重要的基于数据流的检查,例如检查未初始化的值。在此过程中还构建了更加脱糖的 THIR(Typed HIR),THIR 主要用于 pattern checking 检查。

Code generation: 主要基于 LLVM 做代码生成,也支持 Cranelift。

编译入口

当我们运行编译命令 rustc main.rs 时,编译器首先会通过 rustc_driver 这个最上层的组件来处理输入参数,然后调用更基础的组件来启动编译行为。

编译器的入口在于 rustc_driver::main,接着调用:

RunCompiler::new(&args, &mut callbacks).run()

真正的跑编译流程的过程在于 run_compiler,主要流程还是 ParsingAnalysisingLinking。我们看到很多调用是从一个叫做 queries 的东西开始的,比如:

  • queries.parse()
  • queries.global_ctxt()?.enter(|tcx| tcx.analysis(()))

这是 Rust 编译器的一个特点,正在从传统的 pass-based 方式转向 demand-driven,按需编译的主要思路是既然编译是典型的输入输出系统,同一个输入的输出是一样的,所以适合用缓存来减少重复计算。这是算法设计中 Memoization 的思路,详细的设计文档在 rustc-on-demand-and-incremental

好处主要在于增量编译时加快编译速度,结果就是用户更改的少量的代码,编译速度会更快。另外一个原因是这样方便并行编译。

但目前还有很多 phase 并没有完全实现这种按需处理的方式,目前只有 HIRLLVM IR 之间的步骤是查询的。我们可以在这里看到默认的 query provider:

pub static DEFAULT_QUERY_PROVIDERS: LazyLock<Providers> = LazyLock::new(|| {    let providers = &mut Providers::default();    providers.analysis = analysis;    providers.hir_crate = rustc_ast_lowering::lower_to_hir;    providers.output_filenames = output_filenames;    providers.resolver_for_lowering = resolver_for_lowering;    proc_macro_decls::provide(providers);    ....    rustc_codegen_ssa::provide(providers);    *providers});

注意这些 provider 是按照 crate 这个维度来组织的,我在日常开发中经常碰到的一个问题是,如果我切换了 compiler 的代码分支,然后直接进行增量编译,最终链接的时候报错,这大概是因为某些 crate 的代码变了,而缓存的结果是老的,重新 clean 后编译就好了,以后再排查一下具体原因。

query 引入的另外一个问题是导致错误堆栈特别长,在调试过程中经常碰到几百行的堆栈信息,我打算在这个 Issue 里尝试解决。

Lexing

Lexing 的过程和其他编译器类似,我们可以理解为给定字符串的源文件,输出一个 token 的数组。对应的代码在 compiler/rustc_lexer,这个 advance_token 就是读取下一个 token。

但是 Rust lexing 过程中的特殊点在于输出为一个称之为 token 流的东西,advance_token 被一个叫做 tokentrees.rs 的模块调用,处理后的结果是 TokenStream,其实也就是一组 Token,只是定义为一个树形的结构:

pub struct TokenStream(pub(crate) Lrc<Vec<TokenTree>>);

其定义为:

pub enum TokenTree {    /// A single token.    Token(Token, Spacing),    /// A delimited sequence of token trees.    Delimited(DelimSpan, Delimiter, TokenStream),}

至于为什么返回的是这个树形结构,可以参考这里 What does the tt metavariable type mean in Rust macrosTokenTrees 简而言之就是为了处理宏。

为了看看这个 Lexing 的过程,我们可以写个简单的程序来看看中间结果:

fn main() {    let a = 1;    println!("a = {}", a);}

在这里 parse_token_trees修改代码来把 TokenTree 打印出来:

if let Ok(ref token_trees) = token_trees {    debug!("token_trees: {:#?}", token_trees);}

通过环境变量来把编译器运行过程中的中间结果打印出来,重定向到一个文件,运行命令:

RUSTC_LOG=debug rustc main.rs > /tmp/r.log 2>&1

我们可以看到这个程序的 TokenTree 是这样的:

也就是通过分隔符 (...){...}[...] 把 Token 分组,我最近对这个模块做了一些改进和重构,任何分隔符不匹配的问题会报错然后终止编译,主要原因是分隔符的不匹配会让 Parser 构造出完全错误的 AST,这样诊断信息就会非常多,而大多数对开发者没有用。

Parsing

Rust 使用的手写的递归下降(自上而下)方法进行语法分析,解析是按语义构造组织的,可以在 rust/compiler/rustc_parse/src/parser 目录看到以下文件:

  • expr.rs
  • pat.rs
  • ty.rs
  • stmt.rs

我们可以使用以下命令来把整个程序的 AST 打印出来,这对于编译器开发阶段比较有帮助:

rustc ./p/main.rs -Zunpretty=ast-tree > tree.log 2>&1

另外,读这部代码结合 Rust Reference 会容易很多,因为 parser 很多时候就是 reference 的直译,看懂了 reference 就容易看懂 parsing。

错误处理

为什么 Rust 不用那些高级的 parsing 工具而采用手写的方式,我认为一个原因在于手写能给出更好的诊断信息,可以看到 parser 中很多代码在尝试从错误中恢复,比如用户写了下面这个程序:

const FOO: [u8; 3] = { 1, 2, 3 };

当 Parser 处理到 { 这个位置,这里看起来用户想写的是一个数组,但把 [ 写成了 {Parser 中的这段代码会先把当前的状态存储为一个 snapshot,然后尝试 1, 2, 3 是否能 parse 成一个数组元素,如果是则能给出一个更为优化的诊断信息,如果不能则恢复到保存的 snapshot

fn maybe_suggest_brackets_instead_of_braces(&mut self, lo: Span) -> Option<P<Expr>> {    let mut snapshot = self.create_snapshot_for_diagnostic();    match snapshot.parse_array_or_repeat_expr(Delimiter::Brace) {        Ok(arr) => {            // emit better error here            ...            self.restore_snapshot(snapshot);            Some(self.mk_expr_err(arr.span))        }        Err(e) => {            e.cancel();            None        }    }}

Parser 中很多代码都在处理类似这种逻辑。错误处理也是一个很大的话题,在 parsing 这个阶段能做的都是明显的语法层面的处理。

宏展开

Parsing 的过程中会遇到宏,但宏处理需要在 AST 构建之后,所以在这个过程中所有的宏会通过占位符来特殊标识。

相对 Parsing,宏展开是一个更为复杂的过程,AST 有了之后会 driver 会通过一下调用路径来逐个 crate 展开宏:

resolver_for_lowering -> configure_and_expand -> expand_crate -> fully_expand_fragment

fully_expand_fragment 这个函数是宏展开的主要算法,首先找到 AST 中的占位符,维护一个队列,然后不断地去展开直到所有的宏占位符都处理完毕,再统一加到 AST 中去,这是因为宏代码中也可能包含宏?如果某次迭代没有展开一个宏说明有语法问题。

Name resolution

Name resolution 就是解析 AST 中的所有名字,包括变量名、函数名、类型名、生命周期的命名等等。
在宏展开的过程中,我们只处理了 import,而并没有关注所有的名字解析,所有的命名需要等到宏展开处理了之后专门来解析名字,这也是这部分代码很多函数的名字叫做 late_*,很多逻辑在一个叫作 late.rs 的文件里。但我们并没有看到一个 early.rs 的文件,因为被拆分成了三个文件:build_reduced_graph.rs, macros.rsimports.rs

我们来写个程序包含一个明显的变量 a 未定义:

fn main() {    println!("{}", a);}

编译器在编译的过程中肯定会报错,使用以下命令来把第一个错误信息当作一个 bug,这样我们就可以获得这个报错的调用堆栈,这是调试编译器一个很有用的小技巧:

rustc ./p/main.rs -Z treat-err-as-bug=1

通过查看堆栈我们可以看到错误是在这里出现的,因此我们找到了 name resolving 的入口在resolve_crate

/// Entry point to crate resolution. pub fn resolve_crate(&mut self, krate: &Crate) {     self.tcx.sess.time("resolve_crate", || {         self.tcx.sess.time("finalize_imports", || self.finalize_imports());                                 EffectiveVisibilitiesVisitor::compute_effective_visibilities(sel          ...         self.tcx.sess.time("late_resolve_crate", || self.late_resolve_crate(krate));         self.tcx.sess.time("resolve_main", || self.resolve_main());         self.tcx.sess.time("resolve_check_unused", || self.check_unused(krate));         self.tcx.sess.time("resolve_report_errors", || self.report_errors(krate));         self.tcx             .sess             .time("resolve_postprocess", || self.crate_loader(|c| c.postprocess(krate)));     });     // Make sure we don't mutate the cstore from here on.     self.tcx.untracked().cstore.leak(); }

其中 self.late_resolve_crate(krate) 就是按照 crate 逐个去解析里面的 name,而 self.resolve_main() 是找整个程序中是否存在 mainLateResolutionVisitor 就是用来递归地遍历 AST 里的元素,比如 resolve_localresolve_params 等等。

这里有一个很重要的概念叫做 rib,我估计是 Rust internal block 的简称🤔,这里有各种类型的 rib,一个 rib 就是定义了一个命名空间和其对应的 binding:

pub(crate) struct Rib<'a, R = Res> {    pub bindings: IdentMap<R>,    pub kind: RibKind<'a>,}

比如我们写代码中的一个大括号就会引入一个新的 rib,同样的一个函数或者模块的定义会引入对应的 rib。对于代码:

fn main() {    let a = 1;    {        let a = 2;        println!("{}", a);    }}

那如何能找到在 println! 的时候所用的变量 a 呢?因为变量是可以被覆盖的,可以想象这是一个按 scope 从里往外找的过程,从代码上也可以验证这个猜想,resolve_ident_in_lexical_scope 函数就是这样实现的。

在名字解析的过程中,Rust 分别为 types、values、macros 保存了不同的命名空间,因此下面这样的代码虽然看起比较诡异但却是合法的 Rust 代码:

type x = u32;let x: x = 1;let y: x = 2; // See? x is still a type here.

名字解析是非常复杂的部分,光 late.rs 这个文件就有 4000 行代码了。之前我做过一个关于名字解析的 PR,当一个变量没在当前 scope 里找到的情况下尝试去 inner scope 找,如果找到则给出建议。这虽然是个不复杂的 PR,但我通过这个 PR 理解了这块的大致逻辑。

Ast validation

这个阶段没做什么特别复杂的检查,比如这种:

  • no more than u16::MAX parameters;
  • c-variadic functions are declared with at least one named argument;
  • c-variadic argument goes the last in the declaration;
  • documentation comments aren’t applied to function parameters;

AstValidator 实现了各种 check_* 函数,通过 visitor pattern 在 AST 里逐个检查对应的元素,在编译器中最常用的设计模式就是 visitor pattern ,所以在 rust_ast 里定义了这个 Visitortrait

pub trait Visitor<'ast>: Sized {    fn visit_ident(&mut self, _ident: Ident) {}    fn visit_foreign_item(&mut self, i: &'ast ForeignItem) {        walk_foreign_item(self, i)    }    fn visit_item(&mut self, i: &'ast Item) {        walk_item(self, i)    }    fn visit_local(&mut self, l: &'ast Local) {        walk_local(self, l)    }    fn visit_block(&mut self, b: &'ast Block) {        walk_block(self, b)    }    ...}

所有的自定义 Visitor 只需要实现这个 trait 就行了:

impl<'a> Visitor<'a> for AstValidator<'a> {    fn visit_attribute(&mut self, attr: &Attribute) {        validate_attr::check_attr(&self.session.parse_sess, attr);    }    fn visit_expr(&mut self, expr: &'a Expr) {        ...    }    ...}

HIR

HIR 是 rustc 中使用的主要 IR,是在解析、宏扩展和命名解析之后生成的。HIR 的许多部分与 Rust 表面语法非常相似,除了 Rust 的一些表达式形式已被脱糖。例如, for 循环被转换为 loop 并且不出现在 HIR 中,这使得 HIR 比普通 AST 更易于分析。

我们可以使用以下命令来展示一个程序的 HIR :

rustc main.rs -Z unpretty=hir-tree > tree.log

我们可以看到即使一个非常简单的程序,生产的 hir 也是非常长的,因为带了很多编译器里面分析使用的字段,另外 HIR 中也带有对应的代码行,也包括 Span 等这些信息,对生成诊断非常重要。rustc_hir/src/intravisit.rs 定义了一些方便在 HIR 上遍历的 visitor

HIR 和 AST 基本是一一对应的,所以整个转换的过程就是遍历一遍 AST,代码在 rustc_ast_lowering。注意 HIR 里的 HirId 非常重要,这个 ID 是后续使用 HIR 时候经常会用到的,所以必须是唯一的。在 lowering 的过程中通过 next_id这个函数来生成唯一的 ID。

我曾经尝试做过一个比较大的 PR 来保证父节点的 HIR_ID 一定比子节点的小,但是做到后来发现代码中的递归经常需要先创建子节点,然后再创建父节点,这样 HIR_ID 就很难保证顺序,否则代码就改得很难看。如果你感兴趣可以看看能否继续做下去 Assign HirIds in HIR traversal order

语法糖什么的都会在这时候处理掉。

Type Inference

类型推断是自动检测表达式类型的过程,比如以下代码:

fn main() {    let mut things = vec![];    things.push("thing");}

我们并没有显示声明 things 的类型,但是因为后续代码中往 things 里写入了一个字符串,所以 things 的类型可以推断出是 Vec<&str>

Rust 使用的是一个改进版本的 Hindley-Milner (HM) 算法,该算法最先被实现在 ML 系的编程语言中,后来被广泛采用在各种函数式编程语言里。

这块我目前接触也比较少,记得之前做过一个 PR 尝试修复一个 type inference 的小问题,不过没做完 Extend Infer ty for binary operators,问题看起来也比较简单

pub fn myfunction(x: &Vec<bool>) {    let one = |i, a: &Vec<bool>| {        a[i]  // ok    };    let two = |i, a: &Vec<bool>| {        !a[i] // cannot infer type    };    let three = |i: usize, a: &Vec<bool>| {        !a[i] // ok    };    one(0, x);    two(0, x);    three(0, x);}

变量 two 不能被推导出来是因为 i 没有类型,虽然我们知道 a 的类型是 Vec<bool> ,但不能保证 a[i]就是 bool 类型,如果你感兴趣可以试试看能否解决。

Type inference 有其局限性,2015 年 RFC 0803-type-ascription 提出来作为补充,但这个 RFC 实现了之后一直没有稳定,最终社区又提出把这个功能给去掉,而这个工作也涉及到大量的改动:De-RFC 3307: Remove type ascription

MIR

MIR 是比 HIR 更低层次的中间表示,从 HIR 构建。MIR 方便用于控制流分析和代码优化,其中也包括 Rust 特殊的 borrow checking。MIR 的关键特性:

MIR 的一些关键特性是:

  • 基于控制流图
  • 没有嵌套表达式
  • MIR 中的所有类型都是完全显式的

更深入了解可以读官方的这篇文章 Introducing MIR。MIR 都是一些比较原子性的操作,离 LLVM 的 IR 比较近,所以很方便后面代码生成部分。另外为了方便做 borrow checking MIR 也会在插入一些 scope 的标签。

我们可以通过 Rust Playground 查看生成出来的 MIR,如何基于 MIR 做数据流分析可参考 MIR dataflow,在 MIR 上做 borrow checking 也会更精准,NLL(non-lexical lifetime) 就是这样解决的。

Codegen 代码生成

一直到这里为止,编译都是在做数据转换,把代码变成中间层表示,然后抽象的等级越来越低,最后把 MIR 生成 LLVM IR,然后生成二进制文件。

Rust 后端可以是 LLVM、Cranelift 或者 GCC,这些都依赖于第三方库来实现,所以需要最大程度共享一些基础代码,Rust 编译器本身有自己的 LLVM 绑定包。

在这个阶段也做了如下这些事情:

  • 为范性类型替换成具体的类型
  • 为具体类型生成代码称为单态化 (monomorphization)
  • MIR 转换为 codegen IR
  • 调用 codegen 后端生成可执行文件

代码生成的入口点是 rustc_codegen_ssa::base::codegen_crate


总的来说,我理解 Rust 是加了些便于做静态分析的语言特性,比如 lifetime 和 borrow checking 规则,编译器内部也集成了很多静态分析功能。

当然我们只是从很高的维度去快速过了一遍,里面还有些特殊的部分很复杂但我还没开始细看,比如 trait solving

后续继续更新 😁。

苹果:为了安全让 M2 吃灰

作者 yukang
2023年3月7日 07:20

苹果新的芯片性能真是不错,并且续航很可观,所以我最近买了个 M2 Pro。有几年没使用 Mac 系统了,所以日常使用还有些别扭,但最让我闹心是发现了苹果一个让人大跌眼镜的设计,而我几乎没找到关于这点的中文资料,所以写下来分享给你。

我日常会花时间在 Rust 编译器项目上,经常需要编译 rustc 和跑单元测试。单元测试大概是 1.4 w 个测试用例,测试框架会并行跑编译并执行后对比结果。我发现这台 Mac 跑测试一共需要 16 分钟,这是不可接受的,因为我之前使用 WSL 也不过 20-30 分钟左右。我用一台 32 c 64 g 的 Linux VM 跑同样的测试只需要 1.5 分钟。

我这台 Mac 选的配置一般,CPU 核数是 6 性能 + 4 效能,另外内存 32 G,这样算来也不可能有 10 倍的性能之差。在 Rust 代码仓库跑单元测试:

./x test tests/ui --force-rerun

可以通过 htop 看到明显没有充分利用所有的 CPU,上图是 Mac 的系统资源统计,下图是 Linux VM 的:

我实在想不通为什么会这样,因为我之前看到过 Mara Bos 发的 M1 Mac 的数据,她大概只需要 9 分钟跑完所有的单元测试。

然后我在 Rust 开发者论坛 rust-zulip 里发起一个帖子,很快得到了一些开发者的回复。刚开始有人怀疑是 mdworker_shared 进程的问题,这个进程是为 Spotlight 做索引用的,因为跑测试会不断生成新的临时文件,从 htop 上看这个进程会占用不少 CPU。但我把 Spotlight 彻底关闭掉,性能确实有一点点提高,但这明显不是根本原因。

我怀疑是不是测试框架用的 threads 数目不对,看代码是通过这个 get_concurrency 获取的,我通过 RUST_TEST_THREADS 尝试把数目提高,但是也没卵用。

Eric Huss 用的是 M2 Max,他跑测试花费的时间是 9 分钟,这个结果显然也不能匹配上高贵的 Max 配置。

后来有人提到是不是因为 SIPHuss 关闭 SIP 之后跑测试时间立马从 9 分钟减少到 1 分 36 秒 ! 这几乎是 5 倍多的提速。 另外,如果把 SIP 打开但把网络给关闭掉,同样能得到类似的提速。

这就是说跑单元测试的时候系统在不断地发送网络请求,这也解释了为什么我对比国外的用户跑测试所用的时间会更长,因为我走了 VPN 啊!我关闭 SIP 之后测试时间从 16 分钟提高到 153 秒,这可是 10x 的提速!

那么 SIP 是什么?

这东西全称 System Integrity Protection,译为系统完整性保护:

System Integrity Protection (SIP) in macOS protects the entire system by preventing the execution of unauthorized code. The system automatically authorizes apps that the user downloads from the App Store. The system also authorizes apps that a developer notarizes and distributes directly to users. The system prevents the launching of all other apps by default.

During development, it may be necessary for you to disable SIP temporarily to install and test your code. You don’t need to disable SIP to run and debug apps from Xcode, but you might need to disable it to install system extensions, such as DriverKit drivers.

SIP 是 OS X El Capitan 时开始采用的一项安全技术,目的是为了限制 root 账户对系统的完全控制权,也叫 Rootless 保护机制。从文档看出,苹果自家的 Xcode 系统是做了特殊处理的,但第三方软件需要经过 SIP 的检查。

更多细节请参考这篇文章 macOS 10.15: Slow by Design简而言之 SIP 会在我们跑任软件之前,把你的执行文件做一个校验和,然后通过网络请求发送到让人敬畏的苹果服务器,就是为了检测是否是恶意软件!

在我跑单元测试的时候,通过查看 Mac 的系统日志可以发现这么一条关键信息:

log stream | grep Xprotect

XprotectService 这个就是在检查我跑测试用到的一个 dylib 文件。Xprotect 是一个病毒扫描器,它会检查可执行文件是否在已知恶意软件列表中。

这真是个让人无语的设计!

这不仅适用于从网络下载的文件,也适用于你自己编译的程序或者是写的一小段脚本。因此,即使你编写了一行 shell 脚本并在终端中运行它,可能也会有延迟,在 HackerNews 上看到一个中国开发者发的可能有几秒的延迟:

echo $'#!/bin/sh\necho Hello' > /tmp/test.sh && chmod a+x /tmp/test.shtime /tmp/test.sh && time /tmp/test.sh

PS:如果你运行过这个命令把 Terminal 加到可信列表,跑脚步就没这个问题了:

sudo spctl developer-mode enable-terminal

更让人吐血的是,此问题已报告给了苹果,然而苹果回应说这是“设计使然”!而你也会看到更多人在网络上反馈同样的性能问题,比如:

Hugo runs twice as fast in Asahi Linux than macOS on the same M1 Mac system

好了,如果你也会频繁跑大量的程序,可能也会受此影响。为什么我说”可能“,是因为这东西太复杂了,我还没搞清楚所有细节!官方文档关于 SIP 只有寥寥数语,如果你想了解更多关于 SIP 的资料,可以参考这篇博文 System Integrity Protection – Adding another layer to Apple’s security model

当我粗看这篇文章的时候,以为可以配置一下 /System/Library/Sandbox/rootless.conf 就可以忽略某些目录的文件,结果是我太幼稚了。我问 bjorn3 怎么回事,得到的回答是:

The system file protection is only a small part of the protections against malware macOS has. Xprotect is a virus scanner which checks all executables against a liat of known malware. There is signature checking (AMFI) which also checks if the certificate the executable has been signed with has been revoked (using an internet service from apple). This also checks if the entitlements the executable declares are allowed or for example only allowed by apple signed executables (like the SIP bypass entitlement). There is also a check that the application is allowed to access certain protected directories like your documents or images directory. And there are a couple of other checks. These are performed independent of where the executable is stored.

看起来就只有全关闭这条路了?如果你想关闭 SIP,还有那么点麻烦:

  1. 重启 Mac,按住 Command + R 直到屏幕上出现苹果的标志和进度条,进入 Recovery 模式。(如果是新的 Mac 就在启动的时候长按住电源键)
  2. 在屏幕上方的工具栏找到并打开终端,输入命令 csrutil disable
  3. 关掉终端,重启 Mac;
  4. 重启以后可以在终端中查看状态确认。

关闭也许会让你的 Mac 处于裸奔状态,我也不清楚有多大的安全隐患。开启 SIP 只需在上面第 2 步命令改为 csrutil enable 即可。


我上一个 Mac 是 2012 年买的,一共用了六七年,那台 Mac 真是非常耐用,所有的硬件这些年都没出现问题。苹果的硬件一直领先业界几个段位,我上次买是因为 Retina 屏幕,这次买是因为苹果自家的芯片。

有人说 Mac 是最适合开发者的设备,但苹果关心开发者么?我在 Rust Zulip 问一个对 Mac 很熟的开发者,这都快七年了为什么苹果不修复这个明显的问题,他的回答是:

从我作为一个局外人的观察来看,苹果公司不再像以前那样关心开发者了。他们曾经有一流的文档资料,但现在你要是能找到一点点文档就该知足了。

也许在苹果眼里只有使用 Xcode 的开发者才能称之为开发者!否则绝不会弄出这么个脑残设计,事实上很多开发者都没有意识到这是系统的默认行为,这么牛逼的芯片很多时候是在吃灰。

这不是 Secure by Design,而是 Slow by Design


Update:
一个读者指出了更简单的办法,把你信任的工具加入到 Developer Tools:

注意必须通过 UI 设置,这条命令虽然提示设置成功了,但是其实没成功 😂:

为 Rust 做贡献的经验分享

作者 yukang
2023年1月19日 05:36

2022 年下半年我花了很多时间为 Rust 做贡献,最近一个阶段性的收获是我获得了 Rust Foundation 项目资助 🙌。

这个收获完全是副产物,我在 8 月份开始做这些的时候不知道 Rust Foundation 有这类资助,在为 Rust compiler 做了 30 多个 PR 后我发现了这个资助项目,然后很快就发起了申请。我的项目计划是:

To support contributions to the Rust compiler, and to continue to blog about the experience, sharing the learning experience, knowledge, and skills with others.

所以除去写代码,最近几个月我会写一些 Rust 相关的技术文章。我之前写过一篇 我如何学“会”了 Rust ,分享了一些去年通过做开源项目来学习 Rust 的相关经验。

这篇主要集中在如何参与到 Rust compiler 相关的开发中来,但我觉得纯写技术文章会有些单调,而且很多资料都有官方文档,翻译为中文意义不大,所以这篇我主要写自己的感受和经验。

我如何开始

我一直是通过为项目做贡献来学习 Rust,因为纯看书或者文档我觉得收获不大,大概是年纪大了看了就忘记了🐸。

很多事情都不是规划好的,我觉得生活充满了随机性,但我会努力和乐观些,并保持好奇心。好奇心对程序员很重要,这是源动力。

我的第一个 Rust compiler 的 PR 是在 2021 年 9 月做的 Remove duplicated diagnostics 。这是我在使用 Rust 的开发过程中发现的问题,一时兴起翻看 compiler 的代码想知道这个现象是如何产生的,毕竟这看起来很容易 debug。

事实证明我低估了上手的难度,这个问题我大概花了一周的业余时间,期间当然也花了不少时间去看文档,我还尝试使用 gdb 去一行行跟踪代码。最终的修复虽然也比较简单,但是代码 review 来回好多次。

做完第一个 PR 之后,我觉得很有成就感,但随即我就去搞其他的了。一直到 2022 年的 8 月,我偶然又看了几个 issue,感觉有一些 parser 和报错信息相关的问题比较容易解决,所以就又开始做了。过段时间刚好是微软的 hackthon,有两周左右时间可以用来学习感兴趣的东西,所以我的时间比较多地投入到上面,一直到现在都会规律性地给 Rust 做贡献。

我解决的问题比较零碎,很多都涉及 diagnostics,也有些涉及 Infra 之类的,还有些是纯粹编译器里的 bugfix,我喜欢解决一个个独立的 issue,这让我觉得每个新的问题都是一个小谜题,而做稍大一些的改动需要耗费更持久的精力。后续我会试着找一些固定的方向做。

贡献流程

Rust 编译器的仓库里面包含了上万个测试用例,不管是解决 bug 还是做一些新功能开发,最好先写一些最小测试用例,然后不断修改代码、编译、测试,直到用例测试通过。

编译型语言最大的弊端是失去了 interactive programming 的乐趣,特别是 Rust 这种编译特别耗时的语言,不是很适合做探索式编程,我的办法是编译的时候就继续看代码,还有在关键的地方多加一些日志信息。

在这个过程中,最耗时的可能是代码 review 这个阶段,PR 发出来之后 rustbot 会从列表中随机挑选一个 maintainer 来 review 代码,但因为目前很多 maintainer 的 review list 积累过长,所以需要等待很久才能开始。目前社区正在讨论如何解决这个问题 Did we start reviewing PRs slower?,我的解决办法是 PR 发出之后就开始解决其他问题,如果有反馈再切回这个分支来修改。


对于比较大的开源项目,需要用大量时间去和其他人沟通和讨论,所以写作能力很重要。看文档、理解代码、调试代码、和其他人交流,这些是软件开发中的核心能力,在做开源工作时得到很好的提升,可以很快地迁移到其他项目上。

如何选择 issue

大家可能会有个误解,认为要为编译器做贡献必须得对 Rust 很了解。我认为不是的,以我的经验和水平算也不上很资深的 Rust 开发人员,我并没有在实际工作和生产环境使用过 Rust,但对编译器做贡献并不需要对语言本身了如指掌。

如果涉及到语言本身的语义、语法的改动,则需要 RFC 的流程,经过社区大量的讨论,这些对于新手来说相对不容易,但编译器项目里面本身有很多用户发现的问题、流程改进、bugfix、重构、优化等需要做,所以新手最适合从解决 issue 开始。

当掌握了 Rust 的主要的语法,能看懂大部分代码的情况下就可以开始,新手需要通过一些简单的问题来上手,带着问题看代码更有效果。

但刚开始,你可能不知道自己能解决什么问题,可以从 E-mentor, E-easy, E-help-wanted 这类标签去找一些相对简单的问题来解决。但如果找不到适合的,也可以随机找一些自己能看懂的问题,试着去解决。

A-diagnostics 的问题通常是完善或者修复 rustc 的报错信息,这类问题适合新手。Rust 编译器里面很多代码都是为了给用户提供尽量有用的报错信息,Rust 在这块的关注和投入超过绝大部分编程语言,正如 estebank写道:

We spent decades trying to invent a sufficiently smart compiler when we should have been inventing a sufficiently empathetic one.

既然 Rust 学习曲线比较陡峭,我们应该让编译器善解人意。这类问题通常需要去理解 AST,推测用户的编程意图,给他们提供有用的帮助信息。

我们可以通过搜索关键字的方式来找到错误信息对应的代码,但并不是所有的这类问题都容易解决,有的时候需要在不同的阶段中去验证,比如我这个 PR Suggest to use . instead of ::,方法调用报错是在 resolve 阶段出现的,但是我们需要等到 hir typeck 阶段才能去复核是否能给出帮助信息。

有的 ICE 问题也比较容易解决,比如一些 corner case 没有处理的情况,通常有一个粗暴的解决方法,但从各种修复方案中权衡利弊也是门艺术。

如何发现问题

做编译器开发,一个重要的转变是从语言的使用者变成改进者,编程语言是工具,编译器也就是另外一个软件,这都是可以改进的。如果视角变了,就能发现很多问题。

有一天在 Twitter 上看到这个分享,我的疑问是为什么编译器会同时出现两个建议,第一个明显是只适合关心返回结果的情况:

所以我记录下来,随后发起一个 PR 对此做了修复 Properly handle postfix inc/dec

发现问题的另一个来源是日常使用,比如我在日常使用 Rust 时发现如果我少输入了一个 ‘}’,编译器可能无法指出括号不匹配的位置,而且如果源文件很长可能会报出大量错误,这是因为大括号的不匹配没有得到适当的处理,导致 parser 出来的语法树是完全不对的,而编译器总是尝试去从错误中恢复。这种情况下除了指出缺少 ‘}’,给出其他信息都是无法帮助用户尽快修复问题的,所以我做了另一个 PR 去解决这个问题

一些小贴士

Guide to Rustc Development 这个文档需要经常看,这份文档相对源码可能有些地方不够新,但对了解编译器的各个主题概要非常有帮助。

Rust 有一个不那么完整的 Reference,如果你看不懂某部分代码,很可能是不知道相关的名词,这时候翻看这个 reference 就会很有帮助。

在 rustc 的开发过程中,x.py 是经常使用到的命令,我把自己常用的命令写成了一个 justfile 配置文件,这样做测试、查日志、rebase 之类的会比较方便。

在调试过程中,日志加上读代码比 gdb 一行行去跟踪更有用。通过阅读代码,在关键的步骤打印日志,通过运行时的日志去验证自己的猜想,这样我们可以在脑海中获得代码的主要流程和组织结构,而通过 gdb 跟踪容易迷失在具体的细节里。

使用 rust-analyzer 读 Rust 代码很容易,特别是 show call hierarchy 对理解函数调用特别有用。强类型系统对于写代码可能是一个负担,但对于阅读和理解代码绝对有很大的好处,类型就是文档。

在开发过程中如果遇到问题,可以去 t-compiler/help 发帖求帮助,zulip 是 Rust 编译器开发人员使用的讨论工具,你也可以在上面通过用户名找具体的人讨论问题,通常大家都是很乐于帮助的。

另一个好的学习方法是去 review 其他人的 PR,刚开始看不懂没关系,可以看看大致思路。有的 issue 如果我感兴趣但没时间去解决我会点击订阅通知,这样 issue 如果被 close 我会收到邮件,我可能会去看看 PR。

国内开发人员在做开源的时候所会面临的一个问题是语言障碍,这只能通过长时间的不断练习能提高。如果想中文沟通,欢迎找我交流。

总结

如果你对编译方面的开发不感兴趣,也可以试着去找其他领域,Rust 在 Infra、WebAssembly 和 Web 开发、嵌入式、游戏、安全等领域都在快速发展。另外,对 Rust 做贡献不限制于写代码,报出好的 issue、改善文档和翻译、加入讨论这些都属于社区贡献。

如果你只是想了解一些编译相关的知识,这些资料非常适合入门:

创造运气在于多做并且让更多人知道,做开源和分享对程序员来说是一个创造运气的事。希望我的分享对你有用。

我的 2022

作者 yukang
2023年1月3日 06:08

2022 对于我来说是特殊的一年,在这一年里我有失望、痛苦、愤怒,也有不少付出、收获和成长,一年到尾最大的感受是活着不易,庆幸还能写一年的总结。

写作

刚好去年的元旦,我做了一个开始更多写作的决定,在 2022 年的头一个月里,我做到了日更的节奏。而后的半年我保持在一周两篇左右的频率,但是后半年我就开始偶尔写写了,这是因为我有了很多精神内耗,为了缓解内耗我后半年很多空余时间都在编程。

总共写了 68 篇文章,在这近一年的写作过程中,我收获了 3000 个的公众号订阅,博客阅读大概 10 w 多,这对于很多大号来说不值一提,但我自己比较满足了,因我大概也就比较密集地写了半年,另外写作对我来说是业余爱好。在这个过程中我的写作能力得到了提高,在最近的一篇文章中,码农翻身的 liuxin 帮我润色和组织文章,让我意识到写那种广泛传播的文章和写纯干货的文章有很大的区别,文章想要引起传播效应得有一些钩子一样的东西激发读者的情绪,另外起标题真是个技术活。当然我写作并不是完全为了流量,能实践不类型的写作对我来说是一种收获。

在今年我也尝试了一段时间周刊,我的很多读者也是喜欢这种形式。虽然我的 Obsidian 里面还保存了不少我读过的未分享文章,但最近我没有写了,希望新的一年能继续保持下去。我在考虑使用竹白来专门写周刊,也许可以做付费的形式,一种履约也许让我把这件事情更长久地做下去。

Obsidian 是今年我一直在用的工具,除去发布过的文章,我今年做了更多的笔记,所以在我印象中似乎今年的记忆更为深刻些。

通过写作我认识了更多朋友,这是我本来所能预想到的。所以新的一年,我会继续写作,争取能写得更多。

阅读

2022 年我阅读了一些小说,之前分享过我读完了所有余华的作品。根据微信阅读的统计,2022 年我有 210 天阅读过,读了 67 本书,其中读完的只有 37 本。陪女儿练乒乓球的地方旁边刚好有个书店,所以我每周都会去里面逛逛。

今年读的书大多是非技术类的,也许是因为年纪更大了或是因为阅读了这些书,我也会琢磨些之前未细致考虑过的人生问题。


在这些书中我推荐:

  • 纳瓦尔宝典
  • 项塔兰
  • 悉达多
  • 阿诚的棋王、树王、孩子王
  • 余华的书

奇鸟行状录这本书也许是我去年读的最厚的小说,我觉得前 3/4 很好,我喜欢里面关于井的故事,结尾我不喜欢。

阿诚的文字很特别,精细而深刻,人除了生存,总得找一个角落安放精神:

我常常烦闷的是什么呢?为什么就那么想看看随便什么一本书呢?电影儿这种东西,灯一亮就全醒过来了,图个什么呢?可我隐隐有一种欲望在心里,说不清楚,但我大致觉出是关于活着的什么东西。

人总要呆在一种什么东西里,沉溺其中。苟有所得,才能证实自己的存在,切实地掂出自己的价值。

除去阅读,我也会听一些播客,比如文化有限,看电影解说,比如越哥说电影,最近这段时间我喜欢看徐云的骑行流浪的视频。

生活

我家有两个女儿,小的不到一岁半,还处于‘养’的阶段,大女儿五岁多,更多的挑战在于‘育’。家庭占据了我很多时间和精力,特别是从三月份开始我一个人带着大女儿在苏州宅了近两个月。这段时间里我深刻地体会到了带孩子的苦与乐,由于我拙劣的厨艺,女儿瘦了几斤,而我却胖了几斤。

这段时间里,我终于从碗盘不沾进化成了可以做顿家常菜的水平,这也是被逼出来技能吧。我对家庭生活里的琐碎事有了更多耐心,虽然有时候还是自觉做得不够好,但今年是进步最大的一年了。

在健康上,我今年面临的问题是肩胛骨酸痛,这算是职业病了。新冠是今年绕不开的主题,和绝大部分人一样我整年做了 11 个月核酸,结果在 2022 年的最后几天感染了新冠。

新冠感染的第一天我特别难受,妻子还在考虑是不是普通感冒,我很清楚这肯定就是新冠了,我都不必去再去做核酸检测,因为全身酸疼、精力被抽干,这种感受是我之前从未体验过。我的症状只持续了两三天左右,而后妻子和小孩也都开始有了症状,不过幸好都是三天左右就没有特别难受了。

经过两周的恢复,干咳才完全消失,但我感觉体力已经大不如前,比如稍微劳作一下就不想动了,还会偶尔心跳异常,所以至今仍然不敢过多运动。

2022 年里有太多不能明说的敏感词,对未来我仍然比较悲观。之前看过张宏杰的两本书《饥饿的盛世》和《中国人的性格历程》,在这些魔幻和痛苦的日子里,我对此有了更深刻的体会。

我每天都会在 Youtube 上看王局的节目,估计看多了之后自然就会比较悲观,刚开始我还会和相近的朋友分享些看法,后来我也渐渐独自琢磨和消化了。相比蒙住眼睛和耳朵装作什么都不知道,我更愿意选择知道然后悲观。

编程

2022 年也是我这几年做开源最多的一年吧,从这个 Github 上的统计上看,虽然有 12623 个提交,但很多都是我写文章时 Obsidian 的自动备份。

从 8 月份左右开始基本都是在做真的代码提交了。之前我也分享过,今年下半年我主要在给 Rust compiler 做贡献,目前统计大概完成了 70 个 PR。

我算是在用编程来缓解新冠带给我的不良情绪,当我无法改变什么却又忍不住悲观时,写写代码时间就过去了。很多夜里,我一边等待着编译和测试结果,一边打开王局的节目,那些难熬的日子就过去了。

通过给 Rust 做贡献,我不仅找到了编程的那种纯粹快乐,也找到了一个乐于助人的社区,还得到了一些经济上的回报。我一直在践行多元化自己的收入和生活,尝试如何不依赖公司和组织,所以我对这段时间的改变很满意,也许 2023 年还会有更大的变化。


新冠让我感受到了个人在大时代面前的渺小,我对未来不再做过长的规划,因为我们必须学会拥抱不确定性,学着如何过好一天、一周、一个月,把握好当下就是最好的应对措施。

新的一年,希望读更多书、写更多文章和程序,保持健康。

玩了一周 ChatGPT,谈谈我的想法

作者 yukang
2022年12月11日 06:48

Alberto Romero via MidjournyAlberto Romero via Midjourny

看到推特上无数人在晒 ChatGPT 的截图,我也忍不住注册了一个账号,到目前为止我快玩了一周了。刚开始我感觉非常震惊,三观受到冲击,经过这几天的多次调戏发现了些 ChatGPT 的缺陷,兴奋劲过去了之后谈谈我的一些看法。

首先 ChatGPT 的语言理解基本都正确,甚至会根据上下文的不同背景使用不同的措辞,输出的句子也是语法上没有什么问题,而且他还具备一些日常的理解能力:

ChatGPT 善于在你给的简短描述上自由发挥,比如下面这段我让它帮忙写个工作总结,可以看出 AI 在这三个方面做了合理的细化和扩展:

令人印象深刻的是它能围绕一个问题和我进行一定程度地互动:

但是,如果你继续深入地问他感受和观点,它就会陷入两种结果:从训练中积累出来地“理中客”,或说“我是 AI 没有具体感受”。

合理的使用方式

辅助编程

如果我们想查询一个简单的代码片段,ChatGPT 是比较适合的,甚至这种有确定答案的查询其结果比 Google 好。比如这种问题:

但我们并不能完全信任其结果。通常经典的编程问题,比如这种排序算法、不同编程语言里典型的文件操作、如何发起一个 HTTP 请求之类的问题,可以预见网络上这类可用于训练的资料非常多,所以其结果通常是对的。

但某些情况下,ChatGPT 生成的代码时有问题的,甚至是误导性的。比如我同事在日常工作中让他生成一个 PowerShell 连接 SQL 的代码,其结果中有一个伪造出来的参数。

另一个让我更震惊的是,某些情况下 ChatGPT 可以发现程序里的问题,比如我们把这个快排程序刻意加入两个 Bug,AI 居然都能理解并找出来:


这是和 Copilot 最大的区别,不止能帮忙补全代码,也能作为代码 Review 的辅助工具。在编程的时候使用这个工具需要你本身对这块足够了解,否则就会被坑。在学习编程时适当使用也会很不错。

辅助创作

ChatGPT 在根据一些条件去生成文字方面确实有一手,比如@piglei 的这个例子:

这是目前我所看到的类似工具中做得最好的,比之前那些智能写作工具有一个本质上的提高。ChatGPT 似乎收集了很多模板,在适当的时候可以套进去,比如你让他生成一个大学申请书,它就会按照一定的模板去套你的信息。

语言翻译

我真实投入了日常使用的是英语翻译,比如我这周有就用 ChatGPT 来生成了两份英语邮件回复。我对比了翻译的质量,这个结果比 Deep Translator 要好,我基本不用做特别多的修改,就能拿来直接用。英文文章翻译为中文同样结果不错。

缺陷

ChatGPT 会尽量回答问题,就像是一个活了几个世纪的老人,似乎什么知识都能知道一些,然后给出一个大概及格的回答。但有时候它是在瞎忽悠,如果你是外行就不一定发现他在忽悠。

关于事实的询问,它有时候会伪造出来一些看起来合理,但其实是错误的答案:

即使目前这个测试版本还存在很多明显的问题,我认为 ChatGPT 这次的刷屏标志着 AI 应用进入了一个新的阶段。这也让我们再次思考人类的智慧是否可以被替代,我们的工作是不是要丢了。

淘汰人类不一定会吧,但确实会让某些领域失去些魅力。几年前 AlphaGo 在围棋领域击败了最强大的人类选手,一直以来我们认为 AI 在围棋上是不能做到这点的,因为搜索空间太大了,而这确确实实发生了。在 DeepMind 的纪录片里,我看到 AlphaGo 团队的程序员们自己都觉得不可思议,称其本质和原理上是简单的,就是一个基于概率的搜索程序,言语中还透露着一些惋惜,棋艺这个我们人类自豪的智慧艺术竟然这样就被击败了。现在围棋已经被 AI 彻底玩腻,专业选手还得不断学习 AI 下棋的套路。柯洁现在似乎对围棋的乐趣没那么深了,我看到他的一个访谈里谈起围棋,透露着一种无奈和空虚感。

ChatGPT 目前因为所有人都在刷屏一些表现良好的例子,所以可能会被大家高估。我一个高中同学是心理咨询师,看着 AI 如此自然地回答问题有点担心自己失业,所以很想把玩一把。我写了个程序做了个接口转发给她玩,她稍微琢磨了一下,似乎又不担心自己丢工作了。

ChatGPT 强项在于写作,如果我们周围充斥着机器生成的文本,那纯手工写作会显得更有价值?
Paul Graham 最近在推上写到:

If AI turns mediocre writing containing no new ideas into a commodity, will that increase the “price” of good writing that does contain them? History offers some encouragement. Handmade things were appreciated more once it was no longer the default.

And in particular, handmade things were appreciated more partly because the consistent but mediocre quality of machine-made versions established a baseline to compare them to. Perhaps now we’ll compliment a piece of writing by saying “this couldn’t have been written by an AI.”

我赞同这个观点,比如最近我偶然看到 InfoQ 上的这篇文章,很明显是人肉在机器翻译的基础上随便做了点修改,读起来就是那么地别扭,而且因为编辑是外行,文章里面还有些重要的错误。而这就是趋势,我么将来会被越来越多的这样内容充斥。

统计学家I.J. Good于 1965 年提出技术奇点的必要条件──“智能爆炸”概念:

让我们将超级智能机器定义为一种能够远远超过任何人的所有智力活动的机器。如果说设计机器是这些智力活动的一种,那么超级智能机器肯定能够设计出更加优良的机器;

毫无疑问,随后必将出现一场“智能爆炸”,人类的智能会被远远抛在后面。因此,第一台超级智能机器是人类需要完成的最后一项发明,前提是这台机器足够听话,会告诉我们如何控制它。

这最后一项发明是否临近了?我乐观猜想二十一世纪应该还不能实现,程序员这工作应该在我退休之前还是安全的😂?不过我还是很乐于使用这些 AI 辅助工具,这些工具也将渐渐地彻底改变编程,短短半年的时间里 Copilot 已经成为我日常编程中比较依赖的东西。

你对 ChatGPT 有何感想?欢迎留言交流。

Twitter 实习生 George Hotz

作者 yukang
2022年11月30日 01:29

经过一轮又一轮的裁员,Twitter 大批骨干离职。

有人戏称:现在 Twitter 办公室里只剩下两个最“硬核”的程序员了。

这两个人中一个自然是老板 Elon Musk。

另外一个则是 33 岁的 George Hotz,一个周薪 2000 美元的 Twitter 实习生。

这位实习生的年龄着实大了一点儿,并且缺乏前端相关的经验,不得不从了解 JavaScript 和 GraphQL 开始,从头一点点学习 Twitter 使用的编程语言 Java 和 Scala,他甚至在网络上直播自己如何弄明白 Twitter 是如何运行的

这样的人怎么可能入得了 Elon Musk 的法眼,逆行加入 Twitter 呢?

原因很简单,George Hotz 是一位超级技术大牛,网络上随处可见他的神奇经历。

2007 年 8 月 21 日,当时 17 岁的 George Hotz 在自己博客宣布成功破解 iPhone,手机不再局限于 AT&T 网络,而是支持其他 GSM 网络,并在博客上发布了详细的解锁过程和视频,最终这部破解的 iPhone 换了一部跑车和三部新的 iPhone,让他在黑客圈子里声名鹊起。

2009 年,他又开始解锁 PlayStation 3,后来被 Sony 起诉,最终因为黑客圈最大组织 Anonymous 施予的强大压力下和 Hotz 和解。

随后几年他去了高校潜心研究机器学习。

2014 年 7 月,他加入 Google 的 Project Zero,短暂工作后退出。

2015 年,专注于驾驶辅助技术,在网上免费发布了他的自主驾驶代码“openpilot”,声称可以用最小的成本做出个更好的自动驾驶技术。

这给特斯拉带来了巨大威胁,Elon Musk 在 2015 年就想“招安”他,让他来特斯拉做自动驾驶,后来因为种种原因谈崩,George Hotz 自己成立了一个做自动驾驶的公司 comma.ai,因此成为了 Tesla 的对手。前段时间他还在点评 Tesla 发布会时,表示对其机器人产品的不屑。

George Hotz 肯定是不缺工作也不缺钱的人,为什么要在这个时候加入 Twitter 呢?

可能是他感受到 Elon Musk 和他是一类人,他喜欢混乱和挑战,他最近一个多月才开始频繁发推,并且开始体会到社交网络的乐趣。

也可能因为他没经过 996 的毒打,很想体验一把鸡血的高强度推特工作。当那封名为 A fork in the Road 的美国版奋斗者邮件发出后,Geroge Hotz 评论到:

This is the attitude that builds incredible things. Let all the people who don’t desire greatness leave.

随后表示自己不想远程办公,想去旧金山进行全职的实习生工作,正好 Elon Musk 也认为远程工作不靠谱:

另外,他正在从自己创办的公司中退出,似乎是对 自动驾驶失去了兴趣 。当然这并不意味着 comma.ai 失败了,而是他觉得自己对运营一个更为庞大的公司没有兴趣:

It’s no longer a race car, it’s a boat. And steering a boat requires too much damn planning and patience.

所以在这个空档期找一些没做过的事情试试,就是这么任性。在一个代码直播中,他自称多年前也做过一些老派的互联网技术,那时候主要还是用 PHP,现在这些互联网相关的技术他之前没怎么实战过,后台开发、前端、微服务,对些他来说都是没折腾过的,所以称之为 For the glory of the technical challenge

至于有的人对此表示不理解,他的回复是优秀的程序员想干什么就干什么,不需要理由:

作为顶尖黑客,geohot 在互联网上有很多轶事。他如何做到如此出色和有创造力,普通人能从他身上学到些什么?

最近几个月,我在业余时间会去看 George Hotz 的 油管视频 ,感觉非常有趣,也有收获。他在直播里通常会把自己的思路自言自语说出来,而且会时不时停下来谈谈对一些事情的看法,有时候还会哼点小曲。他也是一个非常直率的人,说话风趣,说话语速比较快,很有节奏感,有时候可当作 rap 来听,我们还能锻炼英语听力。

强烈推荐你也去观摩一把,看看顶尖程序员如何工作的。我认为这大概是 Learn by doing 和 Learn in Public 最好的实践。他每次通常会定一个最小目标,然后连续数小时的持续学习,比如:

  • 在直播中开发小巧的、类似 pytorch 的深度学习框架 geohot/tinygrad
  • 在没看过 Clang 和 LLVM 的情况下去尝试实现 C 语言的一个新的语法逻辑
  • 实现一个和自己下国际象棋的 AI 程序
  • 看论文,调试各种机器学习的模型,玩 diffusion

顶尖程序员并不是人坐下代码就噼里啪啦出来了,而是也可能和普通人一样,需要不断地 Google 和翻阅文档,一样可能会混淆行和列,从教程中拷贝粘贴代码,看着报错信息饶头皱眉 wtf,还有程序跑出来正确结果那种孩童般的欢呼雀跃。

这种观感就如同一些观众评论到:

That exact same thing happens to me EVERYTIME. Nice to see someone 10 times smarter than me do the same.

It’s encouraging to see that someone as ridiculously genius as George Hotz still has to Google Python and even struggles with the way stuff works in his program. It definitely made me realize I have just been focusing on memorizing too much stuff when it comes to programming. I just need to make more projects and have more fun doing them! Thanks Geohot!

Dude has an IQ of > 9000

Confuses rows and columns like a normal human Finally,

I feel less shitty now.

当然有的时候你也能看到他速度非常快,敲代码就如同电影里那样 (之前用 Vi,最近改用 VsCode 了):

this guy programs like how hollywood thinks people program

他看文档经常扫一样就知道个大概,看起来就像是凭借直觉在工作,这就是积累下来的自学能力,可以快速迁移到任何项目上,他解释到 Object level skills will die out, metalevel skills will be useful,比如学 Data Science, 我们到底是学了某个公司用的工具,还是去学统计学?前者是 object level 的,后者是 meta level 的。

在这些直播中,我觉得一些比较有趣的观点和片段是:

  • 不折腾编辑器和多屏幕,这些不影响效率, Give me a Macbook Air and a corner。
  • 工作中你也许用不到数学,但学习数学和物理给人一种 Knowing the secrets 的感受,那些给你教条的家长、老师他们可能没你懂世界是如何运行的。
  • 不喜欢远程办公,这让工作感觉像度假,喜欢去公司和同事一起当面工作。
  • 为什么 30 多岁来还去当实习生,薪酬 2000 美金一周 what the fuck, who cares?
  • 我经常搜索一些看起来简单的问题,在别人看来我就是个新手,这不重要,别担心别人的看法 ,关注自己的能力,而不是外在的印象。因为印象和人设是很容易改变的,而能力才是最重要的。
  • 开源代码比内部代码有用,很多公司会把好的代码开源出来 ,而那些内部用的代码质量其实很低。
  • 开发过程中喜欢把主要的步骤用 plain text 写下来。
  • 如何学习编程,想一个自己感兴趣的项目,直接开干,Learn by doing,看编程视频没法学会编程。
  • 推荐的编程语言:Assembly, C, Python, 外加 Haskell 和 Verilog, 你不用对 Assembly 非常精通,但只有懂了 Assembly 才懂计算机底层在干什么,才知道 C 的精妙之处,懂了 C 之后才知道 Python 帮你做了什么,这三门语言是抽象的不同层次。学一下 Haskell 可以帮助你理解编程语言的设计,学些 Verilog 让你知道硬件如何运行。
  • C++ 太复杂,Golang 是给学不会 C++ 的 Google 程序员用的🤣
  • 我们处于编程 2.0 时代,Machine learning 就像是数据驱动编程。
  • 什么是编程,面向新手讲解 what is programming?
  • 相信技术奇异点,两篇改变自己人生的文章,推荐 Staring Into The SingularityUnqualified Reservations by Mencius Moldbug

通过观看这些视频,我感受到他和普通人的明显区别是:极其强烈的好奇心和空杯心态,强大的学习能力和持续专注的能力,这也许是最值得我们学习的。而他去 Twitter 折腾,正如他在博客上写的那样:

I hope that there’s people in the world who get joy from actually doing the thing and not just solving the problem.

译:阅读的必要性

作者 yukang
2022年11月27日 06:49

原文:The Need to Read (paulgraham.com)
作者:Paul Graham
2022 年 11 月

在我小时候读的科幻小说中,总有比阅读更有效率的方式获取知识,神秘的 “磁带” 如程序加载到计算机一般植入人的大脑中。

这种事不太可能很快实现。这不仅是因为我们很难找到阅读的替代品,而且因为即使存在,它也是不够的。阅读关于 x 的内容并不只是教你了解 x,同时还教你如何写作 [^1]。

那又怎样?如果我们找到了更为快速的方式来替代阅读,大家就没必要写得好了,不是吗?

更重要的原因是,写作不仅仅是一种传达想法的方式,也是一种创造想法的方式。

一个好的作家不只是思考,然后写下他的想法,作为一种记录。好的作家几乎总是会在写作的过程中发现新的东西。而据我所知,这种发现是无可替代的。与其他人讨论是发现想法的一个好方法。但即使这样做了,当你坐下来写作时,你仍然会发现新的想法。这种思考只能通过写作来完成。

当然,也有一些思考是可以不通过写作完成的。如果你不需要太深入地研究一个问题,你可以不通过写作也能解决。例如,如果你正在考虑如何连接两台机器的部件,也许写作是无用的;而当一个问题可以被很正式地描时,你可以在头脑中解决;但如果你需要解决一个复杂的、定义不清的问题,写出来总是会有帮助。反过来,这意味着不擅长写作的人在解决这类问题时几乎总处于劣势。

不能写好就不能思考好,不读好就不能写好。这里的 “读好” 是指两个层面上的,你必须善于阅读,而且要读好的东西 [^2]。

如果你只是想获取信息,那有很多其他方法。但是,对于想要获得想法的人来说,阅读是必不可少的。

[^1] 有声读物可以提供优秀写作的例子,但听别人朗读并不能像自己阅读一样教会你写作。

[^2] 这里的”善于阅读”不是指善于机械地阅读,相比起快速阅读,获取文字的含义更为重要。

和 Rust Compiler 开发者面基

作者 yukang
2022年11月24日 07:56

很久没有更新博客了,最近两个月我在开心地写代码,今天想写篇文章,赶紧抓住这个冲动。

上周和 Rust compiler 的一个核心开发 compiler-errors 约了个线上面基。今天想写写我们沟通的主要几个方面,因为大部分都是技术相关的问题,所以我觉得写出来和大家分享一下没关系。

他对 Rust 项目贡献时间刚好满一年,这期间做了 400 多个 PR,效率实在太高。我在 Rust 社区混了两个月了,这期间提交的很多 PR 都是他帮忙 Review 的,开发过程中碰到问题我也会向他请教。有次偶然看到他 Twitter 放开了自己的时间表,任何对 Rust compiler 的开发者都可以约个时间聊聊,所以我就约了个线上会议。

首先我问了一个比较宽泛的问题:你如何调试编译器,因为我发现你解决问题非常快。

compiler-errors: 首先对于一个问题,我会尝试构建一个最小能重现问题的代码用例,根据报错信息或者代码栈看源代码,rust-analyzer 对看代码帮助非常大,我们基本可以很快跳转到任何变量或者函数的定义。使用 VsCode 的 terminal 运行命令,错误栈里的文件信息里面有源代码的路径,ctrl 按下去可以直接跳转到对应的代码行。我对 compiler 的很多部分都了解一些,主要是因为看了不少代码,有时候一看错误信息就能大致判断出问题的位置,如果有必要再去看运行的日志。

VsCode terminal 运行命令直接跳转文件位置这个我学到了,我之前一直习惯在系统终端运行命令,VsCode 只是用来编辑代码。

我:你是否使用 GDB 之类的调试器?
compiler-errors: 我基本不用这个,因为使用 GDB 调试需要另外开一些编译选项,这会让编译变得很慢,而且运行的时候也会变得很慢。我记得很早之前使用过一次,感觉不太好。

我:我进行了两个月的 Rust compiler 开发,所以接触到了很多语言的细节,给我的感受是 Rust 像是一个大杂烩,我能看到 Ruby 的影子,比如链式调用这样的风格,也能看到很多函数式编程的影子,所以这很独特,但我会担心 Rust 未来的发展,是否会太过独特而导致只有一小群人在使用。

compiler-errors: 对此我也不是很确定,确实 Rust 比较复杂,有很多问题还没解决,初学者上手的难度比较高,但一个开发者不用掌握所有 Rust 的边边角角也能开始开发,我很肯定,Rust 对于编写和维护大型的、对性能、安全型要求高的项目来说是非常合适的。比如 Rust compiler 这个项目本身,这么庞大的项目我们在 Review 代码的时候其实是比较简单的,主要看实现的逻辑是否有问题,而不会担心内存方面的问题,而且我们也有信心不断地对代码进行重构。Rust 从学术界的编程语言借鉴了不少东西,比如 OCaml 是一门很精美的语言,但是很多年一来一直对并行这块支持不好,工业界的使用范围也比较少。

这些感受和我基本一致,在这么多年我断断续续的学习 Rust 过程中,我从未掌握过 Rust 的所有内容,但我发现从代码层面理解一个 Rust 项目非常容易,我接触过的几个领域的项目都是如此,比如 wasmer, youki, compiler,因为 Cargo 和统一的代码组织方式,还有 rust-analyzer 这样的工具辅助,理解代码相对来说容易很多。

接着我让他帮忙简单看了看我正在做的一个相对比较大的 PR,而后聊了一下他在 aws 工作的情况,这些就不细写了。

期间也问了一个我觉得自己看代码还没理解的部分,就是 method lookup 的相关实现,他说最好在一个 session 里面来分享这些,这样其他人也可以看到。Compiler team 会定时组织一些技术分享,视频都会上传到 Youtube,感兴趣的可以在这里看:RustcContributor::explore - YouTube

我觉得这种线上面基的经历不错,可以认识一些人,得到一些交流。这一年我基本都在家办公,现实中除了和同事沟通,认识新人的机会比较少。所以我也打算搞一个线上预约,如果想和我交流的可以在这里选择一个时间,我们沟通半小时:https://calendly.com/cyukang/30min

无意识偏见

作者 yukang
2022年10月22日 03:50

最近看到 Hao Chen 在 Twitter 上分享了无意识偏见,Hao Chen on Twitter: “Unconscious Bias 无意识偏见

在外企中这确实算一个必修课,微软入职的时候这是着重培训的一块内容。多元和包容的职场环境,需要员工关注这些细节。

在经过培训之前,“无意识偏见”对我来说很陌生,但回想起来我其实是有过这样的经历的。

我联想到了前公司的一件事情。当时我们在做企业的 IT 安全,解决公司数据的安全问题。我们安全相关的团队经过了一些讨论,我需要把这些东西形成文档。其中有一块是员工的安全管理,我们当时的结论是对于外包人员,需要着重管理,因为外包人员素质和安全意识差,人员流动性高,所以接触的数据需要分级等等。

这些都是我们讨论的东西,所以我写在了我们团队的 Conflence 页面。我刚编辑完 (我猜他是无意间看最近编辑页面发现的),一个外包员工给我发私信,言语中透露着愤怒,谁说的外包人员素质差?

我一下意识到,这样写对他造成了很大的伤害。后来这个员工就离职了,我不确定是不是具体因为这件事导致他的离职,但我很肯定,外包在公司里面会感受到各种隐形的歧视的。

不止我的前公司,我也见过很多其他公司的外包人员的各种待遇,比如同在一个办公室里,但节日礼品、文化衫这些只有正是员工有,如此等等。当然可以从公司角度考虑,需要节省成本,但从工作环境和对人的关怀来说,我们应该努力减少偏见对人的伤害。

其实无意识歧视非常普遍,主要是因为人们习惯用标签和惯性思维。例如一个 HR 筛选简历,他最基本的一个筛选条件是学历、专业等等能迅速做出判断的条件,这也许主要是为了效率。当 HR 把筛选条件扩大到年龄、地域、性别等,我们通常会觉得过分了,但这些规则在社会上一直隐形运行着。

人们通常会对自己的受歧视经历印象深刻,但如果你是歧视者,就会自己做出的歧视行为毫无知觉,大多数人会难以发现自己带着习惯形成的偏见。正如耗子所说,偏见不止是对他人会造成伤害,对自己的认知和进步也会形成阻碍。比如技术上的偏见,抛去应用场景和需求谈技术栈,就会让自己的偏见无意识占了主导,从而做出不好的选择。

我认为偏见主要是会扼杀了好奇心和求知欲,当你把一个对立的标签贴上之后,就认为自己已经足够了解,从而会丢掉去了解的好奇心和动力。

没人会是一个毫无偏见的人,除非他抛去所有生活经验的总结,但尽量客观地看待人和事这个习惯值得培养,于人于己都有好处。

我曾经干了 3 年 EDA

作者 yukang
2022年8月29日 06:01

这周末在上海和几个前同事聚了聚,勾起我的一股回忆。

今天就写写 EDA,因为我的第一份工作就是加入了上海的一个创业公司,我们做的就是 EDA 行业中自动化形式验证工具。

这个行业如今被卡脖子,资本涌入,有人称之为国内的风口,但我加入的那时候就像是一个老鼠洞。我毕业那会儿正好看了《黑客与画家》,所以选择了一个创业公司,走了一条更少人走的路。

不过短短三年,我也只是在这个行业浅尝辄止。下面这些谈谈我的经历和感受,凭回忆写写有可能不够准确。

不得不说,美国人卡得很准,中国不是要大力发展芯片么,没有 EDA 这种工具要做芯片简直就是天方夜谭,EDA 软件实际上已经成为中国高端芯片的命门所在。

电子设计自动化(Electronic design automation,缩写:EDA),这个行业的发展伴随着 1980 年后的芯片革命和硅谷的崛起。芯片本质上是很多个物理逻辑门的组合,在芯片的早期,因为复杂度和集成度远不如现在,设计人员还可以手动完成电路设计和布线。

然后芯片的复杂度越来越高,自然人们开始想,如果能够使用软件来描述硬件设计就好了。1986 年,硬件描述语言 Verilog 推出,1987 年 VHDL 推出,各种仿真器开始出现,这些仿真器可以解析 Verilog/VHDL,并对设计的芯片进行仿真,这样使得芯片设计可以在真正被应用前进行严格的验证。

如今,EDA 工具已经成为芯片设计行业的标准工具,涵盖了芯片设计、布线、验证和仿真的所有流程。

EDA 行业的三巨头是 Synopsys、Cadence、Mentor,这些公司比我们大部分现在的程序员年龄都大,其中 Mentor 成立于 1981 年,另外两个分别成立于 1985 和 1986,这些公司如今已经成为事实上的垄断,占据 80% 左右的市场份额。

2011 年,我加入 NextOp 的时候,公司已经创立了 5 年并开始进入了稳定期。市场人员在美国,主要研发人员在上海,这种模式和现在 zoom 这类公司很像。两个创始人都是 90 年左右去美国,读了博士之后进入了这个行业。

他们发现了一个比较细分的市场,因为日常工作中经常需要人为地去写 Property,所以就想如何能在仿真器运行之后自动生成 Property 就好了。Property 类似于我们写程序中的断言,可以当作硬件的一部分 spec,也可以用于硬件开发中的 regression testing,如果一个断言被触发了,可能是一个 Bug,也可能是一个之前漏掉的 coverage。因为硬件的 Bug 非常非常值钱,如果能在芯片设计阶段发现 Bug,那么这个工具将非常有用,我们的产品名称就叫做 BugScope。

我记得当时我们的一个重要里程碑就是找到了苹果的 Bug,可以感受到公司上下都非常有成就感,因为发现一个苹果的硬件 Bug 可以减少很多可能的损失,这非常能证明工具的价值。

这里面有很多技术上的难点,自动生成 Property 可以用到的输入有两方面,仿真器的运行数据和 Verilog/VDHL 代码。如何把仿真器里的运行数据搞出来,如何节省磁盘,我当时看着那些几十年的 C 头文件,去调试仿真器的 hook 函数,有时候盯着下面这种信号仿真看,如今想来都头大。

更难的是如何去自动发现数据里的规律,结合 Verilog 代码去生成 Property,如何写出足够简单而不会自相矛盾的 Property。这些会涉及到 Model checking、SAT solver 之类的算法,Model checking 的开山鼻祖 E. M. Clarke 为创始人的博士生导师,所以作为了公司的顾问。他因为 Model checking 的开拓性工作获得了 2007 年的图灵奖。

虽然公司很小,但技术氛围很好,有些像个实验室,开发人员基本都是来自中科大、上交大、电子科大。作为刚毕业的小白,我在这个公司待的三年还是能学到了不少东西。里面的代码主要是几十万行的 C/C++,任何产品的 crash 都是在客户的机器上,所以对代码质量要求很高。回想起来 Software Engineering 做得非常不错,代码测试覆盖率几乎 100%,还有一堆 fuzz testing,为了解决内存问题 valgrind 在自动化测试中用了很多。

这个行业门槛太高,因为涉及到多个方面,需要一些硬件背景,最好有一些芯片从业经验,还需要好的软件工程能力。具体到我们的问题,比如 Property 怎么生成,就需要不少行业积累和手工打磨,一个个 case 去琢磨,当时公司 10 来个人也只有两三个做这块。我跟着做过一小段时间,发现自己做不来,我的耐心和相关知识都不够。

EDA 行业那时候就已经非常稳定,黄金时期已经过去。有个老板经常感叹,整个 EDA 行业的大小好不如香蕉行业。

我们那时候已经有一些稳定的客户,最大的客户应该是苹果,在上海的时候我也去 Marvell、中兴这些公司做现场调试。

2013 年我们公司被印度人主导的公司 Atrenta 收购了,过了几年 Atrenta 又被 Synopsys 收购了,我在 2014 年因为想去深圳就离职了。在这个稳定的行业,如果想做也是可以一直做下去的,我之前的同事们,有一部分还在 Synopsys 做,有一部分去了美国,有一部分在国内出来创业一圈,随着我国大力支持 EDA 行业,他们又回到了这个行业继续奋斗。

我国是否能自研出来这些 EDA 工具?我们几十亿,几百亿地往里面砸钱,总能激起一些浪花,民族之光华为总能做出来吧?

我不确定,能不能做出来是一回事,好不好用或者能到什么深度又是另一回事。比如现在国内 EDA 工具的领头华大九天能做出部分 5 nm 芯片的国产替代,但 3nm 及以下的高端芯片就被美国卡脖子了。

EDA 这类工具在硅谷自然生长出来,而不是资本催生出来的,也不是一个或者两个公司做出来的。

行业迅猛发展有其时代的背景,因为有了些实际的需求和一定的行业积累,自然会有些人去解决问题和创新,完善的产权保护机制让人能够去解决一些看似小的问题,成为创业企业养活自己,比如像 Verilog/VHDL 这类的 Parser 是一个小公司 Verific 做的,我现在还记得是因为他们每年给客户送上一张巨大的卡通硅谷地图。

像我所在的公司这种一再被并购,大鱼吃小鱼的过程一直在发生,这些 EDA 巨头就是从无数个收购中发展起来的。

我国不缺软件开发人才和资金,但缺既有软件开发能力和这个行业背景,又能解决一些基础数学问题的人,据说国内 Synopsys 已经被挖走了一大半。也许我们短时间能好好追赶一阵,但彻底解决卡脖子的问题估计需要更多年了。

如今想起还有些怀念,单纯的一段技术工作体验。我那时候还是浮躁,要是能更多一些纯粹的好奇心就好了,这样会有更深入的体验。

但这次我们这几个聚会的同事,大多都跳出了这个行业,主要因为我们对这行没有特别大的兴趣和优势,另外想法比较多吧,总之跳了出来就不可能再回去了吧。

为 Rust 做些小贡献

作者 yukang
2022年8月19日 06:22

有一段时间没有写文章了,最近沉迷于给 Rust Compiler 做些贡献,这里分享一下自己的收获和感受。

契机是那天 Rust Issue 到了 100000这个里程碑,我点进去看了看。想起去年花了一周业余时间做过一个去重复的 diagnostics PR,就顺便看了看些最近的一些 issue,我发现有一个看起来比较适合的 issue,就 assign 给了自己。

过了两天居然在 Teams 收到公司的同事的消息,他问我这个 issue 什么时候能解决,因为 raw-dylib 功能要稳定了,他还在等这个 issue。我平时工作基本不会被催,没想到随便接个开源 issue 会被催,哈哈。于是我很快发了 PR,另外看了看这个 raw-dylib 功能,这涉及到 Rust 链接 dll 相关的,Windows 上不少 Rust 问题依赖这个 RFC。

后面我接着做了几个 diagnostics 方面的 issue,这类问题是最适合 compiler 开发新手的,因为通常来说修复并不复杂。我在这个过程中基本看完了 Rust Parser 这部分的代码。

初学 Rust 一个很重要的技能就是理解 Rust 的报错信息,很多时候是编译器在提示我们写程序。编译器的报错信息特别重要,太少则说明不了问题,太详细则让人抓狂。Rust compiler 在报错这方面真的非常好,基本都是源自于开发者发现了更好的报错方式,自己加上去。

我在做的过程中,发现 Rust 编译器的提示很人性化,比如 Parser 发现你该写 pub 的地方写了个 public ,则会提示你是不是应该写 pub,比如你写了个 import mod,则会提示你是不是应该写 use,甚至发现不容易显示的 Unicode chars,则会提示这里要注意哦。关于生命周期的提示,有的还会加上各种好看的图线标识。

甚至,他们最近开始做 diagnostics 的语言本地化了Diagnostic Translation

另外 Rust 的提示在类型推导后也可以加上更多有用信息,如果我们确定这里的提示就是唯一的修复方法,则可通过 rust-fix 自动修复,所以你能看到 Rust compiler 这个 repo 的单元测试里面有很多 .fix 后缀的对比文件。

我最近在修的另外一个 Bug 是来自 Tikv 项目发现的,当函数参数中有 Arc::default()的时候,从类型分析的结果看,这个参数可以满足多个其他参数,这样在分析缺少的参数给出合适的提示时就会有问题,那个算法导致死循环。我花了比较多时间写出最小化的测试用例,最后给出了一个修复。我还挺喜欢分析这类 Bug,像小说一般充满了悬疑。

在这段时间里,我也和一些公司里全职做 Rust 相关的同事聊了聊,发现微软已经有几个组在全职做了,主要集中在 Rust 和 Windows、开发工具相关方面。另外和社区里其他几个开发者沟通了一下,华为的也有一些。

Rust 纯粹是互联网上自由生长出来的一门语言,创始人早已经退出主导,主要的核心成员都是社区自由组成的,这里并没有一个绝对意义上的独裁者。我碰到的几乎都是在凭热情做贡献,比如最近一年很活跃的 compiler-errors 这个开发者,有一次我催他 review PR,他说我不是全职在做 Rust,所以时间需要自己安排,不要催。我接着和他聊了聊,他的日常工作和 Rust 完全无关,花这么多时间就是爱好而已。

这里足够开放,基本上你想参与到 Rust 开发中来,这个门槛是不高的。

我并不是鼓吹 Rust 有多好,Rust 自然还有很多问题,学习成本比较陡峭,而且也并不适合很多日常项目。如果从功利角度考虑,投入产出比不高。

我只觉得 Rust 很好玩,又足够开放,吸收了多年编程语言方面的理论,完全出自开源社区和一线开发者,一切讲究实用,所以又没有 OCaml、Haskell 那种学究气,对编程语言感兴趣的朋友可以多关注一下。

为什么我能沉下去做一些看似繁琐的开源工作,这里有几个方面:

第一,我本身对编程语言的实现挺有兴趣的,几年前我基本看完了 EOPL 这本书,也做了很多里面的小解释器。可以说,编译这块算是程序员的一个小浪漫。带着问题看代码比较容易看进去,看完 Parser 这块之后,我打算再看看类型分析。

第二,Rust 这几年工具链有很大提高,比如 Rust compiler 这样的大项目,VSCode + Rust Analyzer 就能很好应对,几乎能做到所有的变量跳转,函数调用跳转和调用关系分析,类型提示等等。Rust 在这样的大型项目和多人维护的项目上能体现出优势,有编译器的和类型系统的帮助,查阅代码和写代码体验和效率都好很多,这与我日常工作中需要在一堆年代久远的 PowerShell 中翻来覆去爽太多了。

第三,最近两年在日常工作中,我接触到了大量历史悠久的代码,这些代码其实很丑陋,但每年能为公司赚不少钱。我们常说提高编程技能需要向优秀的代码学习,但我发现被迫接触一些丑陋的历史代码对编程的心性大有裨益,因为今后你看很多代码都美得很,编程和调试时候的耐心好了很多。

感兴趣的可以交流交流🙌

让 Obsidian 朗读你的文字

作者 yukang
2022年7月29日 01:31

让写作更好的一个简单粗暴的办法是成为自己的读者,不断重读自己的文字,不断地去修改。

这不止是让文字变得更易读,也是一个和自己对话的过程,Paul Graham 在Putting Ideas into Words中写道:

The real test is reading what you’ve written. You have to pretend to be a neutral reader who knows nothing of what’s in your head, only what you wrote. When he reads what you wrote, does it seem correct? Does it seem complete? If you make an effort, you can read your writing as if you were a complete stranger, and when you do the news is usually bad. It takes me many cycles before I can get an essay past the stranger.

我习惯使用微信读书的 AI 语音来听书,后来我发现微信读书还能订阅公众号,所以尝鲜用 AI 语音来朗读自己写的文章,感觉很神奇,像是请了一个旁人来朗读。

我们通常写了文章之后会自己在心里默读,但一个真实的声音读出来会更容易发现问题:

  • You will find your voice.
  • You will find mistakes and unnecessary words and sentences.
  • You will make your writing easy for reading.

所以,我前段时间写了个 Obsidian 插件来实现朗读功能,代码在这里obsidian-speech

写完之后我想提交到官方插件市场,这时才发现已经有人做了个同样功能的插件,然后就不想提交了。但我还是喜欢自己的实现,因为里面有些小优化。

优化一,在阅读的过程中自动判断出英文段落,因为用中文语音去朗读英文会显得不够协调。

优化二,高亮当前朗读的段落,这样就能快速定位。

但 Obsidian 浏览器里的 AI 语音质量明显没有微信读书里的自然,支持的语音种类也少,微信读书应该是做了不少优化。

这个小插件还有不少可以继续细化的地方,比如自动跳过内嵌的代码部分,如果你感兴趣一起来完善吧。

❌
❌