一张表里绝大多数元素是零。逐项保存、逐项相乘,看起来很浪费。我们可以只保存非零值,但接下来会遇到一个新的问题:这些值原本在哪里?
预计用时:40 分钟。打开 空白的代价,先把非零比例阈值设为 10%,再调到 100%。阈值控制确定的生成规则,实际非零数量由当前输入决定。
本实验的稠密向量用 Float64Array 保存每个位置。压缩后用两段数组:Uint32Array 保存索引,Float64Array 保存对应值。索引按递增顺序排列。
例如 [0,5,0,0,2] 变为索引 [1,4] 与值 [5,2]。每个非零项需要 4 字节索引加 8 字节值。忽略对象头与临时数组后,总数据区大小为 12×非零项数,稠密形式则是 8×长度。
所以,当非零比例高于约三分之二时,这种具体表示的索引成本可能超过删掉零的收益。这只是当前格式的数据区比较;矩阵的 CSR、位图与其他压缩格式有不同边界。
稠密点积把对应位置相乘后求和。压缩后,两个列表中的第 k 个非零值未必来自同一位置,不能直接按压缩数组的下标配对。
sparseDot 用 i、j 两个游标读两份索引:相同则相乘并同时前进;左边较小则只推进左边;右边较小则只推进右边。这样只在原位置相同处执行乘法,其余项贡献零。
这减少了乘法,却增加索引比较、分支和位置读取。大量不规则访问可能不利于缓存与向量化。保存更少的数据也不能单独证明计算更快。
页面的转换要先扫描两份 256 项输入,共检查 512 个位置。10% 阈值的固定样例中,稀疏点积只做 6 次乘法,另执行 45 次索引配对循环;结果为 37。数据区从 4096 字节变为 612 字节。
把阈值调到 100%,两种计算仍得到相同点积,但稀疏数据区变成 6144 字节,比稠密形式更大。零值位置减少以后,索引没有相应消失。
若上游本来就产生压缩格式,转换成本可能已经不在当前查询里;若每次只算一次再丢掉,扫描和分配就必须算进去。比较范围决定结论的用途。
稀疏矩阵通常还要记录每行从哪里开始,或者以块为单位压缩。这里产生了更大的设计空间:元素稀疏不等于块稀疏;有规则的空白与随机空白对硬件的影响也不同。
类似地,一个在渐近乘法次数上更省的算法,可能需要额外的加法、临时空间和数据搬运。例如快速矩阵乘法的研究会同时面对规模交叉点与数值行为。独立算法课堂可以沿这条线比较,而本节先把“省了什么、增加了什么”写清楚。
选择一个任务,分别列出准备成本、单次成本、存储成本和失效条件。可以用前缀查询,也可以用稀疏点积。至少给一组有利输入、一组不利输入,以及一个输出正确性标准。
对稀疏点积额外验收:全零向量、没有共同非零位置的两向量、只有最后一项相遇,以及全非零输入。任何一项都不能靠假设“总有一对能匹配”跳过。
此时可以在确定候选表示与使用条件处停下。下一单元继续调整目标:如果任务允许一定误差,是否还需要一次算到底?