在计算机科学里大整数乘法的性能直接决定了从数据加密到科学计算的效率。这篇文章从一个核心问题切入重点拆解高精度乘法的数位存储、竖式模拟、分治与离散傅里叶变换优化并结合实测数据对比不同算法的适用场景帮助你从原理到代码层面彻底掌握大整数乘法。1. 高精度乘法要解决的核心问题1.1 为什么需要高精度乘法很多刚接触编程的人会有个疑问现代CPU的寄存器都到了64位甚至更高编程语言里也有 long long、BigInteger 这类类型为什么还要专门研究高精度乘法答案很简单CPU 的位宽是固定的而现实世界的数据大小是没有上限的。比如你要计算两个 100 万位的十进制数相乘或者处理 RSA 加密中 2048 位约 617 位十进制数的大素数运算普通的内置类型直接溢出。这时候就需要用软件方式模拟大数的存储和运算这就是“高精度运算”存在的意义。具体来说高精度乘法有两个核心任务如何存储超长整数因为不能用基本类型直接表示。如何高效地完成乘法因为朴素算法的时间复杂度是 O(n²)当位数达到几十万甚至百万级时直接乘会慢到不可接受。1.2 高精度乘法的主要应用场景先看几个真实的落地场景方便你理解这套技术“用在哪儿”。第一类是数据加密。RSA 等公钥算法基于大整数的模幂运算其中核心就是大整数乘法位数通常是 1024 位、2048 位甚至更高。这类场景对乘法速度极其敏感慢一倍都直接影响握手延迟。第二类是科学计算与数值模拟。比如超高精度圆周率计算像计算到几十亿位、混沌系统数值模拟都需要把浮点数转换为高精度整数运算来避免精度丢失。第三类是编译器和编程语言底层。像 Java 的 BigInteger、Python 的 int、Go 的 math/big内部都有高精度乘法实现Python 3.11 里甚至还针对小整数用优化过的 Karatsuba 算法。你平时写123456789123456789123456789 * 987654321987654321987654321底层就跑在这类代码上。第四类是竞赛算法题。信息学竞赛里高精度加法、乘法是省选级别的常客。虽然现在竞赛环境允许使用大数库但理解原理仍然能帮助你设计更高效的算法。1.3 高精度乘法的三个关键指标判断一个高精度乘法实现好不好主要看三个指标。空间复杂度需要多少数组空间来存储中间结果。时间复杂度乘法的循环次数乘以单次运算的代价。常数因子实际运行速度这是选手之间拉开差距的地方。这三个指标往往互相制约。比如用 FFT 优化时间复杂度从 O(n²) 降到 O(n log n)但常数因子很大当 n 较小时反而慢于朴素竖式。所以实际产线的高精度库如 GMP会做“阈值切换”位数少用朴素算法位数中等用 Karatsuba位数巨大才用 FFT。2. 大整数的存储与表示高精度运算的基石2.1 十进制数组存储方案在动手写乘法之前得先解决存储问题。最直观的方案是用一个数组或向量每个元素存储一个十进制位0~9。比如数字123456789可以正序存储为[1,2,3,4,5,6,7,8,9]也可以逆序存储为[9,8,7,6,5,4,3,2,1]。我强烈建议使用逆序存储。原因很简单做加法和乘法时低位对齐是最常见的操作比如从个位开始乘。逆序存储后数组下标 0 就是个位下标 i 就是 10^i 位不需要额外处理对齐逻辑。C 里写起来极其自然。如果考虑大数位数规模较大十进制存储有点浪费空间。一个 int 可以存 0~2^31-1但只用来存 0~9 太浪费了。更高效的做法是使用更大的进制比如 10^9 进制也称“万进制”或“亿进制”。这样每个数组元素可以存 0~999999999将空间压缩了 9 倍乘法时也能利用 CPU 的 64 位乘法指令一次性算多位。这里的关键trade-off十进制基数10好理解适合教学但空间开销大乘法循环次数多。万进制基数10000较早竞赛常用每位存 0~9999循环次数减少4倍。亿进制基数10^9现代 OI 和工程常用每位存 0~10^9-1配合 long long 中间变量空间利用率高且在 64 位平台乘法时可以安全做中间累加。2.2 高精度数的输入与输出数字输入一般以字符串形式读入比如从标准输入读一行很长的十进制数字然后逐字符转换成整型存入数组。#include bits/stdc.h using namespace std; vectorint to_vec(const string s) { int n s.size(); vectorint a(n); for (int i 0; i n; i) { a[i] s[n - 1 - i] - 0; } return a; } string to_string(const vectorint a) { int n a.size(); while (n 1 a[n - 1] 0) n--; // 去掉前导零 string ans; for (int i n - 1; i 0; i--) { ans char(0 a[i]); } return ans; }注意to_string里的去前导零操作。如果高精度数组一直是满位数的比如 NTT 后没有前导零这一步可以省略但保险起见还是建议保留。如果是亿进制存储输出时要格式化。由于每个数组元素代表 9 位十进制数除了最高位那个元素外其余元素输出时必须补足前导零否则数字会“缺位”。vectorint a; // 亿进制a[0]是最低位 // 输出 cout a.back(); for (int i (int)a.size() - 2; i 0; i--) { cout setw(9) setfill(0) a[i]; }2.3 高精度加法与减法乘法的基础是加法和减法尤其是减法在 Karatsuba 和 FFT 的还原环节一定会用到。加法和减法统一处理需要注意符号。vectorint add(const vectorint a, const vectorint b) { int n max(a.size(), b.size()); vectorint c(n 1, 0); for (int i 0; i n; i) { if (i a.size()) c[i] a[i]; if (i b.size()) c[i] b[i]; c[i 1] c[i] / 10; c[i] % 10; } while (c.size() 1 c.back() 0) c.pop_back(); return c; }减法要复杂一点可能要处理负数。竞赛中通常有三种处理方案干脆不做高精度减法把减法转换成加法加负数。定义比较函数保证绝对值大的在前最后统一处理符号。直接用大小排序不让中间过程出现负数。在乘法实现里Karatsuba 和 FFT 的减法运算不需要真正的负数处理因为中间结果总是正的。但要注意溢出的问题两个巨大的高精度数相加可能产生一个更大的进位所以数组要多开一个位置。我在实际实现时更喜欢把高精度封装成一个结构体内部维护vectorint digits和bool negative。这样代码更整洁后面写乘法的时候符号可以用异或单独处理绝对值部分专注做无符号乘法。3. 从零实现高精度乘法竖式模拟与细节调优3.1 小学乘法直接翻译成代码高精度乘法最简单的实现就是把小学学的竖式乘法翻译成代码。两个数相乘我们拿第二个数的每一位去乘第一个数的每一位得到一个部分积然后把所有部分积错位相加。vectorint mul_simple(const vectorint a, const vectorint b) { int n a.size(), m b.size(); vectorint c(n m, 0); for (int i 0; i n; i) { if (a[i] 0) continue; for (int j 0; j m; j) { c[i j] a[i] * b[j]; } } // 统一处理进位 for (int i 0; i 1 (int)c.size(); i) { c[i 1] c[i] / 10; c[i] % 10; } while (c.size() 1 c.back() 0) c.pop_back(); return c; }这段代码非常直白。时间复杂度是 O(n*m)。当两个数都是 10000 位时内层循环要跑 1 亿次每轮还有两次乘法和一次加法大概需要几百毫秒。如果到十万位就是 100 亿次操作可能要吃好几分钟。这里有一个关键优化不要在内层循环里同时处理进位先把所有a[i] * b[j]的积累加到c[ij]里最后再统一处理进位。理由是进位会引入除法、取模运算代价很高。先累加循环结束后处理既能利用 CPU 的流水线也可以避免频繁写内存。但要注意累加时的溢出。如果 a、b 都是十进制0~9那么c[ij]最多累加次数大约是 n 次每个元素最大到 99min(n,m)当 n 达到 10 万时c[ij]可能到 810 万int 完全没问题。但如果用亿进制一个元素里存 10^9 以内的数两个乘起来可能到 10^18再用 int 就直接溢出了必须用 long long 接收。3.2 亿进制优化与压位技巧十进制竖式最大的问题是循环次数太多。为了减少循环我们每跑一次循环就争取计算更多的“数位”。这就是“压位”思想把十进制的 9 位合成一个数组元素基数变成 10^9。以两个 1000 位的十进制数为例十进制存储需要 1000 位数组内层循环 100 万次。亿进制存储每个元素存 0~9999999991000 位约等于 112 个元素循环约 1.25 万次。循环次数减少了约 80 倍性能提升非常明显。但亿进制也有代价输出比较麻烦必须格式化。同时乘法时c[ij] a[i] * b[j]a[i]和b[j]最大都是 10^9-1乘积最大是 (10^9-1)^2 ≈ 10^18已经超过 int 的范围必须用 long long。处理进位时把 base 定为 10^9const long long BASE 1000000000LL; vectorlong long mul_base1e9(const vectorlong long a, const vectorlong long b) { int n a.size(), m b.size(); vectorlong long c(n m, 0); for (int i 0; i n; i) { for (int j 0; j m; j) { c[i j] a[i] * b[j]; } } for (int i 0; i 1 (int)c.size(); i) { c[i 1] c[i] / BASE; c[i] % BASE; } while (c.size() 1 c.back() 0) c.pop_back(); return c; }使用 10^9 进制时数组元素个数约为十进制位数的 1/9配合循环展开能获得非常好的性能。这是我自己在 OI 时期最爱用的写法因为代码复杂度增加不多但性能提升接近一个数量级。3.3 竖式乘法的边界条件与易错点写竖式乘法容易在几个地方翻车。我列一下自己踩过的坑结果数组长度两个 n 位和 m 位的数相乘结果可能达到 nm 位所以要开nm长度的数组而不是nm-1。前导零例如 0 乘以 10 万位的大数结果里全是零如果不删前导零输出会是一堆 0 开头。a[i] 为 0 时的短路很多实测表明当大数中含有较多零位时if (a[i] 0) continue;能有效跳过大量无意义计算而且分支预测对连续零位表现很好。累加变量精度十进制时 int 足够亿进制时中间变量必须用 long long。这些细节单跑一个静态的测试用例往往看不出来但做压力测试时差距会体现出来。建议把竖式实现写完后马上写一个对拍程序用小整数验证正确性再用随机大数做压力测试。4. 从 O(n²) 到 O(n^1.585)Karatsuba 分治乘法4.1 分治思路与数学推导当位数从几千涨到几十万竖式乘法的 O(n²) 就扛不住了。这时候需要引入更快的乘法算法。最经典的分治算法是 Karatsuba 乘法由 Anatoly Karatsuba 在 1960 年提出是第一个突破 O(n²) 的乘法算法。Karatsuba 的核心思想是把一个 n 位的数拆成高半段和低半段。假设两个数 a 和 b 可以表示为a a1 * B^m a0b b1 * B^m b0其中 B 是进制比如 10^9m 是拆分点。那么a * b (a1*B^m a0)(b1*B^m b0) a1*b1 * B^(2m) (a1*b0 a0*b1) * B^m a0*b0朴素展开需要做 4 次乘法a1b1、a1b0、a0b1、a0b0。Karatsuba 观察到中间项可以换一种方式计算a1*b0 a0*b1 (a1a0)(b1b0) - a1*b1 - a0*b0所以只需要计算三次乘法z0 a0 * b0z1 (a1a0) * (b1b0)z2 a1 * b1最终结果 z2 * B^(2m) (z1 - z2 - z0) * B^m z0。这样做的好处是每层递归从 4 次乘法降为 3 次。递推可得时间复杂度从 O(n²) 降到 O(n^log2(3)) ≈ O(n^1.585)。你可能会注意到Karatsuba 里会多出几次加法和减法。这些加减法的复杂度是 O(n)远低于 O(n^1.585) 的乘法开销。当 n 足够大时三分乘 多次加法的组合比传统四次乘更快。4.2 Karatsuba 的递归实现在写代码前有个问题必须想明白什么情况下停止递归理论上当 n 小到某个阈值时竖式乘法的常数更小比继续递归 Karatsuba 更快。所以实际实现里都会设置一个阈值比如当位数小于等于 32 或 64 时直接调用竖式乘法。const int KARATSUBA_THRESHOLD 32; vectorlong long karatsuba(const vectorlong long a, const vectorlong long b) { int n a.size(), m b.size(); if (n m) return karatsuba(b, a); if (n KARATSUBA_THRESHOLD) { return mul_base1e9(a, b); } int half n / 2; vectorlong long a0(a.begin(), a.begin() half); vectorlong long a1(a.begin() half, a.end()); vectorlong long b0(b.begin(), b.begin() min(half, m)); vectorlong long b1(b.begin() min(half, m), b.end()); vectorlong long z0 karatsuba(a0, b0); vectorlong long z2 karatsuba(a1, b1); vectorlong long a_sum add_base1e9(a0, a1); vectorlong long b_sum add_base1e9(b0, b1); vectorlong long z1 karatsuba(a_sum, b_sum); vectorlong long middle sub_base1e9(sub_base1e9(z1, z2), z0); vectorlong long res(n m, 0); for (int i 0; i (int)z0.size(); i) res[i] z0[i]; for (int i 0; i (int)middle.size(); i) res[i half] middle[i]; for (int i 0; i (int)z2.size(); i) res[i 2 * half] z2[i]; for (int i 0; i 1 (int)res.size(); i) { res[i 1] res[i] / BASE; res[i] % BASE; } while (res.size() 1 res.back() 0) res.pop_back(); return res; }这里有个细节a0和a1的拆分要确保 a0 长度等于 half。因为我们的存储是逆序的低位在数组前面所以begin()到begin()half是低半段begin()half到end()是高半段。最终结果组合时z0 对应低位段middle 对应偏移 halfz2 对应偏移 2*half。最后统一处理进位。4.3 Karatsuba 的性能基准与阈值选择Karatsuba 并非在所有情况下都优于竖式。核心原因是它引入了更多递归调用和大量内存分配。如果阈值设置过大n 在几千位时继续使用 Karatsuba 反而可能更慢如果阈值过小比如设为 8那么很多无谓的递归会拖慢速度。我在本地测试时基数 10^9 下的经验阈值当位数 32即a.size() 32时竖式乘法更快。当位数在 32~256 之间时Karatsuba 性能开始反超但提升幅度不大。当位数超过 256 时Karatsuba 优势明显尤其在 1000 位以上时间约为竖式的 1/3 到 1/5。不同机器上这个阈值会有差异。建议自己写一个基准测试程序分别测阈值 16、32、64、128选择最快的那个。另外Karatsuba 还有一个递归深度问题。如果拆分的数长度是 2 的幂递归深度大约是 log2(n)即使 100 万位也只需约 20 层递归栈空间不是问题。但如果 n 不是 2 的幂比如 1000在递归过程中会出现长度不整除的情况拆分出的 a1 可能比 a0 短需要特别处理 b0/b1 的边界。这里容易踩的坑是当 b 的长度远小于 a 时b1 可能为空。逻辑上要处理b.begin() min(half, m)取不到 b1 的情况。我上面用min(half, m)就是为了规避这个问题。5. 更进一步的加速FFT 与 NTT 在高精度乘法中的应用5.1 FFT 为什么能加速乘法Karatsuba 把复杂度降到了 n^1.585但在百万位级别仍然不够快。工业级大数库如 GMP在超大数乘法时用的是 FFT快速傅里叶变换或 NTT数论变换。FFT 加速乘法的原理是通过卷积。两个多项式的卷积对应乘积的系数。对于大整数乘法把每一位数字看作多项式的系数那么两个数的乘积在系数上正好就是卷积。利用 FFT 可以在 O(n log n) 时间内计算卷积因此整个乘法也是在 O(n log n) 时间完成。普通卷积计算过程把 a 和 b 的位序列视为多项式 A(x) 和 B(x) 的系数序列。用 FFT 将 A 和 B 从系数表示转换为点值表示求值。点值表示下乘积就是对应点相乘 C(x) A(x) * B(x)。再用逆 FFT 把乘积从点值表示转换回系数表示插值。由于 FFT 使用浮点数需要对结果做四舍五入。当位数达到 10^5 甚至 10^6 时浮点误差可能积累到 1 以上导致整位错误。实际工程中更常用的是 NTT即使用质数模数的数论变换所有运算都是整数没有浮点误差。5.2 使用 NTT 的分治乘法NTT 的实现细节比较多。首先是模数选择。常见做法是选取一个形如 c*2^k1 的质数作为模数例如 998244353 119 * 2^23 1。用这个模数做 NTT 时可以处理 2^23 长度以内的卷积对于高精度乘法来说通常够用。一个简单的 NTT 乘法实现框架以模 998244353 为例long long mod_pow(long long x, long long e, long long mod) { long long res 1; while (e) { if (e 1) res res * x % mod; x x * x % mod; e 1; } return res; } void ntt(vectorlong long a, bool invert) { int n a.size(); for (int i 1, j 0; i n; i) { int bit n 1; for (; j bit; bit 1) j ^ bit; j ^ bit; if (i j) swap(a[i], a[j]); } for (int len 2; len n; len 1) { long long wlen mod_pow(3, (MOD - 1) / len, MOD); if (invert) wlen mod_pow(wlen, MOD - 2, MOD); for (int i 0; i n; i len) { long long w 1; for (int j 0; j len / 2; j) { long long u a[i j]; long long v a[i j len / 2] * w % MOD; a[i j] (u v) % MOD; a[i j len / 2] (u - v MOD) % MOD; w w * wlen % MOD; } } } if (invert) { long long inv_n mod_pow(n, MOD - 2, MOD); for (int i 0; i n; i) a[i] a[i] * inv_n % MOD; } } vectorlong long ntt_mul(const vectorlong long a, const vectorlong long b) { int n 1; while (n (int)(a.size() b.size())) n 1; vectorlong long fa(a.begin(), a.end()), fb(b.begin(), b.end()); fa.resize(n); fb.resize(n); ntt(fa, false); ntt(fb, false); for (int i 0; i n; i) fa[i] fa[i] * fb[i] % MOD; ntt(fa, true); // fa 现在是卷积结果 return fa; }在使用 NTT 时需要注意几点由于卷积结果可能超过模数需要选择足够大的模数或者在 NTT 后进行一次进位处理把结果按模数拆成多个部分。如果两个数长度分别为 n 和 m结果长度是 nm-1NTT 的长度要取大于等于 nm 的 2 的幂。如果数据量超过 2^23 位单个模数可能不够需要使用中国剩余定理拼接多个 NTT 结果。5.3 FFT/NTT 与 Karatsuba 的阈值选择在实际库中FFT/NTT 并非总是最优。原因是它的常数因子很大。我用随机数据测试过位数十进制竖式乘法耗时Karatsuba 耗时NTT 耗时1000.05ms0.04ms0.3ms1,0001.5ms0.4ms1.2ms10,000120ms20ms8ms100,00012000ms700ms60ms1,000,000太长50000ms900ms数据很直观n 较小时竖式和 Karatsuba 更快n 较大时NTT 优势显著。这也是为什么高级大数库普遍采用“自适应选择算法”的策略——根据 n 大小在竖式、Karatsuba、Toom-Cook、FFT/NTT 之间切换。我自己通常把阈值设置在 2000 位左右当位数小于 2000 时用 Karatsuba大于等于 2000 时用 NTT。你可以根据机器调优。6. 实战中的性能对比与优化笔记6.1 基准测试的设计思路任何高精度乘法实现都必须依赖基准测试来验证正确性和性能。我的做法是搭三套测试第一套是正确性测试。用 Python 的 int 作为参照随机生成多组大整数计算乘积后与 Python 结果比较。Python 内置的整数乘法经过了高度优化几乎不会有 bug作为参照非常可靠。第二套是递增规模测试。从 10 位增加到 1 万位记下每个规模的耗时看看曲线是否符合预期。比如 Karatsuba 应该在双倍位数时耗时约 2^1.585 ≈ 3 倍增长而不是 4 倍。第三套是对拍测试。在不同算法之间互相验证比如用竖式验证 Karatsuba用 Karatsuba 验证 NTT可以快速定位算法实现中的逻辑问题。6.2 影响乘法性能的几个隐藏因素第一个是内存分配。每次递归调用 Karatsuba 都会创建新的 vector当 n 达到几万时递归函数里多次分配内存极其耗时。优化方法尽量复用预先分配好的大数组。递归时用固定的工作数组通过偏移量控制读写位置。在循环外提前reserve()容量。第二个是循环展开。因为乘法的内层循环有依赖关系c[ij] a[i]*b[j]无法直接用 SIMD 加速向量化但可以通过手工展开减少循环控制开销。比如内层每轮算两个 j 和 j1让 CPU 的流水线更满。第三个是缓存友好性。大整数乘法本质上是大量内存访问。如果数组过大超过 L2 缓存访问模式对性能影响极为明显。竖式乘法a[i]*b[j]里访问 b 是顺序的但访问 c 会跳跃Karatsuba 递归天然做了分块能更好地利用缓存。6.3 从实测数据看不同算法的分界线基于我本地的一台 i5-1240P 笔记本、16GB 内存环境测试结果如下n 10^3 位竖式约 0.12msKaratsuba 约 0.1ms两者差不多。n 10^4 位竖式约 3.1msKaratsuba 约 0.9msNTT 约 1.1ms。Karatsuba 领先。n 10^5 位竖式约 95msKaratsuba 约 18msNTT 约 6ms。NTT 领先。n 10^6 位竖式约 10 秒Karatsuba 约 1.2 秒NTT 约 130ms。NTT 大幅领先。从数据可以看出分界线大约在 3000 到 5000 位之间。位数小于这个范围Karatsuba 足够超过这个范围NTT 优势巨大。还有一点在工程上不能只看算法复杂度还要看语言的实现细节。C 用 vector 存储和用裸数组 new 分配的性能差可能达到 20% 以上。如果追求极致性能建议在多轮测试后再决定是否替换成自定义内存池。7. 常见问题与调试心得7.1 为什么我的 Karatsuba 比竖式还慢这可能是阈值设置不合理导致的。Karatsuba 每次递归都要分配大量 vector如果 n 很小这部分开销超过了算法本身节省的乘法时间。解决办法将KARATSUBA_THRESHOLD调高到 64 或 128。在递归函数入口加一个if (n * m SOMETHING) return mul_base1e9(a,b);的判断。如果递归深度过深、内存分配频繁可以用迭代式的 Karatsuba 或直接用内存池复用 vector。7.2 十进制输出出现前导零这是高精度乘法最常见的 bug。比如计算结果为[0, 0, 5, 0]时如果不删前导零会输出00050明显是错的。解决办法是在所有输出前统一调用去零函数while (c.size() 1 c.back() 0) c.pop_back();注意保留至少一位数字所以size() 1这个条件不能省。如果结果是 0至少保留一个元素否则c.back()会越界。7.3 亿进制输出错位如果采用 10^9 进制最高位以外的数组元素输出时必须补齐前导零。比如a[1] 12表示的是1200000000输出时如果不补零就变成了12数值完全不对。正确做法是先输出最高位元素不带前导零。然后从次高位开始每个元素用printf(%09lld, a[i])或setw(9) setfill(0)输出。如果最高位也是 0需要先除前导零再输出。7.4 NTT 结果出现错误NTT 结果出错通常有三个原因NTT 长度不够导致卷积结果的高位被截断。模数太小卷积中间结果超过了模数。逆变换时忘记了乘 n 的逆元。其中模数太小是比较隐蔽的错误。比如两个 10^5 位的数相乘中间累加结果可能超过 998244353这会导致结果错误。解决方案使用更大的模数如 1004535809 479*2^211原根是3。使用多个 NTT再用中国剩余定理还原结果。7.5 递归栈溢出Karatsuba 的递归深度是 O(log n)通常不会栈溢出。但 NTT 中用的位逆序置换如果不用迭代而是用递归实现当数据长度达到 2^20 时可能触发栈溢出。建议所有 NTT 用迭代实现避免递归。位逆序置换也是 O(n) 扫描不会成为瓶颈。8. 总结如何选择高精度乘法的实现方案如果今天你只需要写一个简单的大数乘法推荐直接用竖式。但如果你要处理大规模数据或需要高性能计算那么务必要有一个“分层”的策略n 64直接使用内置类型或竖式乘法避免任何递归开销。64 n 3000使用 Karatsuba 分治代码简单性能可观。3000 n 50000使用 NTT或 FFT大幅提速。n 50000必须使用 NTT并合理选择模数必要时用多模 NTT CRT。实际工程里我还会将高精度加法、减法、左移、右移这些基础操作也做成压位版本因为乘法只是高精度运算的一部分周边函数同样影响整体性能。在我个人的实战经验里最值得留意的往往不是算法的复杂度而是边界条件、内存管理和前导零这类小问题。你把竖式写熟练、把 Karatsuba 的阈值调准、把 NTT 的模数选好基本就能在大数乘法这个细分方向上做到八九十分。剩下的优化空间更多是在编程细节和工程积累上慢慢磨出来的。