2、裁剪的数学基础:信息论视角下的冗余度量与去除

聊系统裁剪,很多人第一反应是“删代码”、“砍功能”。但删什么、留什么,凭感觉可不行。我做了这么多年系统优化,踩过最大的坑就是——凭直觉裁剪,结果把核心逻辑给裁没了。

后来我意识到,裁剪背后得有数学撑腰。信息论,就是那把尺子。

2.1 信息熵:系统里到底有多少“废话”

信息论里有个核心概念——熵。说白了,熵就是“不确定性”的度量。一个系统里,不确定性越高,信息量越大;反之,全是重复和冗余,熵就低。

举个例子。你写了一段日志:

用户A登录成功
用户A登录成功
用户A登录成功
用户A登录成功
用户A登录成功

这5条日志,信息量几乎为零。因为你看第一条就知道后面四条长什么样。用信息论的话说,这个系统的熵很低。

熵的公式长这样:

H(X) = - Σ p(x) * log₂ p(x)

其中 p(x) 是某个事件发生的概率。如果所有事件概率相等,熵最大;如果某个事件概率为1,其他为0,熵就是0。

我在项目中遇到过一件事。一个实时监控系统,每秒上报1000条指标。我一看,其中800条都是“CPU使用率=45%”。这800条数据的熵几乎为0。后来我们只保留了变化点,系统负载直接降了70%。

核心观点: 系统裁剪的本质,就是移除那些熵值低的、重复的、可预测的数据或逻辑,保留高信息量的部分。

2.2 条件熵与互信息:剪掉“已知”的冗余

光看单个变量的熵还不够。系统里变量之间往往有依赖关系。这时候就要用条件熵和互信息。

条件熵 H(Y|X) 表示:已知X的情况下,Y还有多少不确定性。如果X能完全决定Y,那 H(Y|X) = 0。换句话说,Y就是X的冗余。

互信息 I(X;Y) 则衡量X和Y共享的信息量。公式:

I(X;Y) = H(X) - H(X|Y)

我举个例子你就明白了。一个电商系统里,有“订单状态”和“支付状态”两个字段。你想想看,如果订单状态是“已支付”,那支付状态必然是“成功”。这两个字段的互信息极高。

我曾经在一个老旧系统中看到,一个实体类里同时存了 birthDate 和 age。age 完全由 birthDate 计算得出。这就是典型的冗余——条件熵为0。

实战技巧: 分析系统时,先画出变量之间的依赖图。凡是存在函数依赖关系的字段或逻辑,都可以考虑裁剪掉一个。我一般用互信息矩阵来扫描,一目了然。

2.3 冗余度公式:量化你的系统有多“胖”

信息论里有个现成的冗余度公式:

R = 1 - H_actual / H_max

其中 H_actual 是系统实际熵,H_max 是系统在理想均匀分布下的最大熵。R 越接近1,说明系统越冗余;越接近0,说明越紧凑。

我拿一个配置文件来演示:

# 原始配置(冗余度高)
server.host = 192.168.1.1
server.port = 8080
server.protocol = http
server.timeout = 30

# 裁剪后配置(冗余度低)
host = 192.168.1.1
port = 8080
protocol = http
timeout = 30

原始配置里,“server.”前缀重复了4次。如果系统里只有这一个server,那这个前缀就是冗余。H_actual 降低,R 值升高。

指标 原始配置 裁剪后
总字符数 72 52
唯一Token数 8 4
冗余度R 0.38 0.12

你看,裁剪后冗余度从0.38降到了0.12。系统更“瘦”了,但信息一点没丢。

注意: 冗余度不是越低越好。有些冗余是为了容错、可读性或性能。比如缓存数据,虽然冗余,但能提升响应速度。裁剪时一定要权衡。

2.4 知识体系:信息论裁剪框架

下面这张图,是我自己总结的信息论裁剪框架。每次做系统裁剪前,我都会过一遍这个流程。

信息论裁剪框架 原始系统 计算熵 H(X) 与互信息 I(X;Y) 识别冗余:条件熵≈0 / 互信息≈H(X) 裁剪决策:保留高熵,移除低熵 裁剪后系统 每步都要量化验证

这个框架的核心逻辑很简单:先度量,再分析,最后决策。我见过太多人跳过前两步,直接上手删代码。结果呢?删完系统跑不起来,又得加回去。

2.5 实战:用信息论裁剪一段代码

光说不练假把式。我们拿一段真实代码来开刀。

// 原始代码:冗余度极高
function getUserInfo(userId) {
    let user = database.query("SELECT * FROM users WHERE id = " + userId);
    let name = user.name;
    let email = user.email;
    let age = user.age;
    let birthYear = user.birthYear;
    let currentYear = new Date().getFullYear();
    let calculatedAge = currentYear - birthYear;
    return {
        name: name,
        email: email,
        age: age,
        birthYear: birthYear,
        calculatedAge: calculatedAge
    };
}

这段代码里,age 和 calculatedAge 理论上应该相等。如果不等,说明数据不一致。但系统里同时保留这两个字段,就是冗余。条件熵 H(calculatedAge | age) = 0。

裁剪后:

// 裁剪后:移除冗余字段
function getUserInfo(userId) {
    let user = database.query("SELECT name, email, birthYear FROM users WHERE id = " + userId);
    let currentYear = new Date().getFullYear();
    let age = currentYear - user.birthYear;
    return {
        name: user.name,
        email: user.email,
        age: age,
        birthYear: user.birthYear
    };
}

你看,去掉了 age 字段的存储和 calculatedAge 的重复计算。信息量没变,但代码更简洁了。

我的习惯: 每次重构前,先跑一遍信息熵分析。把系统中所有字段、函数、配置项列出来,计算它们的互信息矩阵。那些互信息接近1的,就是裁剪候选。这招帮我省了不少事。

2.6 避坑指南:信息论裁剪的边界

信息论不是万能的。我踩过几个坑,分享给你:

  • 过度裁剪导致可读性下降:有些冗余是为了让人看懂。比如变量名 userName 比 un 冗余度高,但可读性更好。裁剪时得留一手。
  • 忽略时序依赖:信息论只看静态分布,不看时序。有些数据现在冗余,未来可能有用。比如日志里的时间戳,单独看每条都差不多,但分析趋势时必不可少。
  • 误把高熵当高价值:熵高不代表有用。随机噪声的熵最高,但毫无价值。裁剪时得结合业务语义。

我曾经犯过的错: 有一次我把一个系统的错误码从100个压缩到10个,因为很多错误码的互信息极高。结果运维排查问题时,根本分不清具体是哪种错误。后来我学乖了——裁剪前先问自己:这个冗余是给机器看的,还是给人看的?

信息论给了我们一把尺子,但尺子不能代替判断。用数学度量冗余,用经验决定去留。这才是系统裁剪的正确姿势。