Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

ProGuard can cause incorrect calculations

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
like image 542
Dmitry Avatar asked Dec 13 '13 18:12

Dmitry


1 Answers

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.

like image 132
Dmitry Avatar answered Sep 22 '22 12:09

Dmitry