整数在计算机中是以补码的形式存储的, 补码和原码的区别在负数上。 下面代码上展示了10这个整数在计算机中的二进制是怎么表示的。
public static void main(String[] args) {
//1010 32位只输出了4为, 其他的28为都为零
System.out.println(Integer.toBinaryString(10));
//11111111111111111111111111110110
System.out.println(Integer.toBinaryString(-10));
}
针对负数的表示数字的含义, 可以用如下公式计算具体的数字:最高位取负1, 其他位取正, 整体想加, 就得到了负数的数字。如上面的-10可以用如下公式计算 -1 x 231 + 1 x 230 + .....+ 0 x 20。补码表示法有个好处是, 正负数可以直接做加法运算, 比如上面的正10和负10相加后, 就是0。
现实世界中, 无论是整数还是有理数都有无数个, 那如何在计算机中通过有限的存储位数表示更多的数。32位能够表示的整数是40亿多点, 计算机中浮点数的本质采用的是科学计数法的方式来表述更多的数计算公式: (−1)s×1.f×2e。储存结构如下图所示:
public static void testEqual(){
float f1 = 0.3f;
float f2 = 0.6f;
float f3 = f1 + f2;
//注意这里不能用等于好, 大概率是不等于的
if(f3 == 0.9f){
System.out.println("f3 equal 0.9");
}
}
public static void testPrecision(){
float a = 2000 * 10000f;
float b = 1.0f;
float c = a + b;
System.out.println("c is " + c);
float d = c - a;
System.out.println("d is " + d);
}
上面的情况经常出现在机器学习的场景下,机器学习要计算海量样本,于是会出现几亿个浮点数的相加。每个浮点数可能都差不多大,但是随着累积值的越来越大,就会出现“大数吃小数”的情况。
public static void testPrecision2(){
float sum = 0.0f;
for (int i = 0; i < 2000 * 10000; i++) {
float x = 1.0f;
sum += x;
}
System.out.println("sum is " + sum);
}
解决方法Kahan Summation 算法:
public static void kahanSummation(){
float sum = 0.0f;
float delta = 0;
for (int i=0; i<20000000; i++)
{
float addNum = 1.0f; // 需要加进去的数字
float totalNum = addNum - delta; // 当前加进去的数字和上次计算的误差之和(这里的delta必定是负值)
float tmp = sum + totalNum;
delta = tmp - sum - totalNum; // 计算丢失的精度
sum = tmp;
}
System.out.println("sum is " + sum);
}