为什么无论输入如何,大量的计算循环都会产生相同的输出?

Why do large loops of calculations generate same output regardless of input?

我将这些 运行dom 操作的大循环用于基准测试(只是因为我很好奇)并且 运行 变成了我不明白的东西。无论我为大循环输入什么,它总是产生相同的结果。这是我正在谈论的部分的代码。

public int stuff;
public int result;

myJavaProgram(String[] cmdArguments)
{ 
  stuff=1;
  superLoopCalcualtion();
  System.out.println(stuff+" converted to result:"+result); 

  stuff=12345;
  superLoopCalcualtion();
  System.out.println(stuff+" converted to result:"+result); 

  stuff=9823450;
  superLoopCalcualtion();
  System.out.println(stuff+" converted to result:"+result); 
}  

public void superLoopCalcualtion()
{
  int a=stuff;
  int b=a+99;   
  int z=0;
  for (z = 0; z < 200000; z++) 
  {
    a=a+22;
    b=a*44;
    a=b+1234;
  }
  result=a;
} 

输出是这样的

1 converted to result:1398361394
12345 converted to result:1398361394
9823450 converted to result:1398361394

不可能是对的...对吧??

您在这里看到的是一个(伪装的)溢出,它导致有效乘数为 0,从而有效地结束了循环中的值更改。

实际溢出

为了看到这一点,让我们考虑一个使用 bytes 的简化代码示例,它允许循环更短:

byte a = stuff;
for (byte z = 0; z < 8; z++) {
    a = (byte) (a * 2);
}

我省略了每次迭代都以十进制和二进制形式很好地打印数字的代码。以下是 stuff 为 1、11 和 127 (Byte.MAX_VALUE) 的结果:

1
---
0: 1        00000001
1: 2        00000010
2: 4        00000100
3: 8        00001000
4: 16       00010000
5: 32       00100000
6: 64       01000000
7: -128     10000000
8: 0        00000000

11
---
0: 11       00001011
1: 22       00010110
2: 44       00101100
3: 88       01011000
4: -80      10110000
5: 96       01100000
6: -64      11000000
7: -128     10000000
8: 0        00000000

127
---
0: 127      01111111
1: -2       11111110
2: -4       11111100
3: -8       11111000
4: -16      11110000
5: -32      11100000
6: -64      11000000
7: -128     10000000
8: 0        00000000

要理解这一点,请考虑将二进制数乘以 2 从右边添加一个 0。如果我们持续这样做,我们会将左侧的数字“推”到我们的数据结构可以容纳的范围之外。对于 byte,该范围是 8 位。这样,8 乘以 2 后,我们保证无论前面包含的数字是什么,现在都是 0。继续没有效果,因此我们停滞不前(或陈旧,无论术语是什么)。

将有效位推到“可见”范围之外称为溢出,因为二进制表示不能包含它们并且它们...溢出。在十进制中,这会导致符号 1 的变化。如果你看一下 1 的例子,那个溢出只发生在最后一次迭代,因为数字足够小,这相当于说它右边有很多空闲 space。另一方面,127 立即溢出,因为它是 byte 的最大值,也就是说,需要所有位。


1 在 Java 中,所有数字都已签名。

正在应用您的代码

从这里开始,逐步增加复杂性直到我们到达您的代码,但基本现象是相同的。

对于初学者来说,我们可以通过使用 shortintlong 来增加我们的二进制表示能力,但这只会延迟不可避免的事情。我们需要 12, 32 and 64,而不是需要 8 次迭代。

接下来,我们可以更改乘数。偶数只是 2 的乘法,所以我们会得到相同的结果。请注意,对于 2^n 的特殊情况,我们总是会更快地得出结果,因为我们只是有效地减少了迭代次数。但是,对于奇数,我们将 永远不会 达到(十进制)0;溢出将始终跳过它,我们将再次达到起始编号。这是 stuff = 1(字节),乘数为 3,用于 64 (Byte.MAX_VALUE / 2 + 1) 次迭代:

1
---
0: 1        00000001
1: 3        00000011
2: 9        00001001
3: 27       00011011
4: 81       01010001
5: -13      11110011
6: -39      11011001
7: -117     10001011
8: -95      10100001
9: -29      11100011
10: -87     10101001
11: -5      11111011
12: -15     11110001
13: -45     11010011
14: 121     01111001
15: 107     01101011
16: 65      01000001
17: -61     11000011
18: 73      01001001
19: -37     11011011
20: -111    10010001
21: -77     10110011
22: 25      00011001
23: 75      01001011
24: -31     11100001
25: -93     10100011
26: -23     11101001
27: -69     10111011
28: 49      00110001
29: -109    10010011
30: -71     10111001
31: 43      00101011
32: -127    10000001
33: -125    10000011
34: -119    10001001
35: -101    10011011
36: -47     11010001
37: 115     01110011
38: 89      01011001
39: 11      00001011
40: 33      00100001
41: 99      01100011
42: 41      00101001
43: 123     01111011
44: 113     01110001
45: 83      01010011
46: -7      11111001
47: -21     11101011
48: -63     11000001
49: 67      01000011
50: -55     11001001
51: 91      01011011
52: 17      00010001
53: 51      00110011
54: -103    10011001
55: -53     11001011
56: 97      01100001
57: 35      00100011
58: 105     01101001
59: 59      00111011
60: -79     10110001
61: 19      00010011
62: 57      00111001
63: -85     10101011
64: 1       00000001

我不想过多地讨论位数学,因为我觉得此时它超出了问题的范围。可以这么说,在 MAX_VALUE / 2 + 1 次迭代中,您将再次达到起始编号(并且对于一些编号也在这之前)。

关键是你的 44 是偶数,所以你得到了停滞的结果。

现在开始你的加法运算。在乘法之前和之后,尽管您经常使用它们,但它所做的只是将结果改变一个常量。效果保持不变。考虑

for (byte z = 0; z < 10; z++) {
    a = (byte) (a + 1);
    a = (byte) (a * 2);
}

结果是

1
---
0: 1        00000001
1: 4        00000100
2: 10       00001010
3: 22       00010110
4: 46       00101110
5: 94       01011110
6: -66      10111110
7: 126      01111110
8: -2       11111110
9: -2       11111110
10: -2      11111110

所以我们在 -2 上停滞不前。在十进制中,您可以使用循环公式轻松地看到这一点:(-2 + 1) * 2 = -2。您在循环中“随机”选择数字导致(确定性地)在 ~15 次迭代后确定数字 1398361394 (使用 longs 将通过一些迭代延迟此结果)。只需通过迭代进行数学迭代,您将得到如上所示的循环公式。

结束时间

小心溢出!请确保您选择的数据结构始终足以包含您正在使用的数字范围。最坏的情况是,您拥有(非原始)类型 BigInteger 以获得任意精度(但速度要慢得多)。无论上面讨论的任何参数如何,一旦溢出,您的数学结果将是错误的(除非您是故意进行溢出数学运算)。