Pregunta

I have met a pretty strange bug. The following small piece of code uses a rather simple math.

protected double C_n_k(int n, int k)
{
  if(k<0 || k>n)
    return 0;
  double s=1;
  for(int i=1;i<=k;i++)
    s=s*(n+1-i)/i;
  return s;
}

Edit Using ProGuard can make it go wrong on some devices. I have it confirmed on HTC One S Android 4.1.1 build 3.16.401.8, but judging by e-mails I got, a lot of phones with Android 4+ are affected. For some of them (Galaxy S3), american operator-branded phones are affected, while international versions are not. Many phones are not affected.

Below is the code of activity which calculates C(n,k) for 1<=n<25 and 0<=k<=n. On device mentioned above the first session gives correct results, but the subsequent launches show incorrect results, each time in different positions.

I have 3 questions:

  1. How can it be? Even if ProGuard made something wrong, calculations should be consistent between devices and sessions.

  2. How can we avoid it? I know substituting double by long is fine in this case, but it is not a universal method. Dropping using double or releasing not-obfuscated versions is out of question.

  3. What Android versions are affected? I was quite quick with fixing it in the game, so I just know that many players have seen it, and at least the most had Android 4.0

Overflow is out of question, because sometimes I see mistake in calculating C(3,3)=3/1*2/2*1/3. Usually incorrect numbers start somewhere in C(10,...), and look like a phone has "forgotten" to make some divisions.

My SDK tools are 22.3 (the latest), and I have seen it in builds created by both Eclipse and IntelliJ IDEA.

Activity code:

package com.karmangames.mathtest;

import android.app.Activity;
import android.os.Bundle;
import android.text.method.ScrollingMovementMethod;
import android.widget.TextView;

public class MathTestActivity extends Activity
{
  /**
   * Called when the activity is first created.
   */
  @Override
  public void onCreate(Bundle savedInstanceState)
  {
    super.onCreate(savedInstanceState);
    setContentView(R.layout.main);
    String s="";
    for(int n=0;n<=25;n++)
      for(int k=0;k<=n;k++)
      {
        double v=C_n_k_double(n,k);
        s+="C("+n+","+k+")="+v+(v==C_n_k_long(n,k) ? "" : "   Correct is "+C_n_k_long(n,k))+"\n";
        if(k==n)
          s+="\n";
      }
    System.out.println(s);
    ((TextView)findViewById(R.id.text)).setText(s);
    ((TextView)findViewById(R.id.text)).setMovementMethod(new ScrollingMovementMethod());
  }

  protected double C_n_k_double(int n, int k)
  {
    if(k<0 || k>n)
      return 0;
    //C_n^k
    double s=1;
    for(int i=1;i<=k;i++)
      s=s*(n+1-i)/i;
    return s;
  }

  protected double C_n_k_long(int n, int k)
  {
    if(k<0 || k>n)
      return 0;
    //C_n^k
    long s=1;
    for(int i=1;i<=k;i++)
      s=s*(n+1-i)/i;
    return (double)s;
  }

}

main.xml:

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
              android:orientation="vertical"
              android:layout_width="fill_parent"
              android:layout_height="fill_parent"
  >

  <TextView
    android:layout_width="fill_parent"
    android:layout_height="wrap_content"
    android:id="@+id/text"
    android:text="Hello World!"
    />
</LinearLayout>

Example of wrong calculation results (remember, it's different every time I try it)

C(0,0)=1.0

C(1,0)=1.0
C(1,1)=1.0

C(2,0)=1.0
C(2,1)=2.0
C(2,2)=1.0

C(3,0)=1.0
C(3,1)=3.0
C(3,2)=3.0
C(3,3)=1.0

C(4,0)=1.0
C(4,1)=4.0
C(4,2)=6.0
C(4,3)=4.0
C(4,4)=1.0

C(5,0)=1.0
C(5,1)=5.0
C(5,2)=10.0
C(5,3)=10.0
C(5,4)=30.0   Correct is 5.0
C(5,5)=1.0

C(6,0)=1.0
C(6,1)=6.0
C(6,2)=15.0
C(6,3)=40.0   Correct is 20.0
C(6,4)=90.0   Correct is 15.0
C(6,5)=144.0   Correct is 6.0
C(6,6)=120.0   Correct is 1.0

C(7,0)=1.0
C(7,1)=7.0
C(7,2)=21.0
C(7,3)=35.0
C(7,4)=105.0   Correct is 35.0
C(7,5)=504.0   Correct is 21.0
C(7,6)=840.0   Correct is 7.0
C(7,7)=720.0   Correct is 1.0

C(8,0)=1.0
C(8,1)=8.0
C(8,2)=28.0
C(8,3)=112.0   Correct is 56.0
C(8,4)=70.0
C(8,5)=1344.0   Correct is 56.0
C(8,6)=3360.0   Correct is 28.0
C(8,7)=5760.0   Correct is 8.0
C(8,8)=5040.0   Correct is 1.0

C(9,0)=1.0
C(9,1)=9.0
C(9,2)=36.0
C(9,3)=168.0   Correct is 84.0
C(9,4)=756.0   Correct is 126.0
C(9,5)=3024.0   Correct is 126.0
C(9,6)=10080.0   Correct is 84.0
C(9,7)=25920.0   Correct is 36.0
C(9,8)=45360.0   Correct is 9.0
C(9,9)=40320.0   Correct is 1.0

C(10,0)=1.0
C(10,1)=10.0
C(10,2)=45.0
C(10,3)=120.0
C(10,4)=210.0
C(10,5)=252.0
C(10,6)=25200.0   Correct is 210.0
C(10,7)=120.0
C(10,8)=315.0   Correct is 45.0
C(10,9)=16800.0   Correct is 10.0
C(10,10)=1.0
¿Fue útil?

Solución

Android team member has posted a possible solution in a comment to my issue. If I add android:vmSafeMode="true" to application element of manifest-file, all calculations are performed correctly. This option is not well documented and honestly I do not know how much will it affect the speed, but at least the math will be correct. I will mark it as correct answer until a better one is found.

Otros consejos

The original code and the processed code work fine on the Java VM and on most Dalvik VMs, so it must be valid. If the processed code produces spurious results on a few Dalvik VMs, chances are that the problem is caused by the JIT compiler in those VMs. Google's Android team should then look into it.

The most obvious optimization that ProGuard applies here is inlining the method. A few branch instructions and local variables are reordered in the final bytecode, but the execution flow of this small piece of code is fundamentally the same. It's difficult to determine how ProGuard could avoid the problem. You could disable the optimization step entirely.

You could check if inlining the code manually causes the same problems, without ProGuard (the problem doesn't seem to occur on my devices).

(I am the developer of ProGuard)

This all turns out to be a JIT compiler bug which the ProGuard optimizations just happened to trigger.

As the AOSP issue explains:

There was a window in the Jelly Bean release time in which the JIT would mistakenly optimize away uses of floating point double constants which were identical in the low 32 bits (and also had a few other conditions met). The defect was introduced in late November of 2012 in the internal Google tree, and in February of 2013 in aosp. Fixed in April of 2013.

More detailed explanation in this other AOSP issue.

Licenciado bajo: CC-BY-SA con atribución
No afiliado a StackOverflow
scroll top