Just How Fast Is That Code?

Recently I added a CreateBigInteger() method to my SecureRandom class in the Spackle library I have on CodePlex. You give it a number that defines how many digits you want, and that's what the method gives you, like this:

var number = new SecureRandom().CreateBigInteger(16);
// number will be something like this: 4796435982615820

My initial implementation was a very naive one:

public BigInteger GetBigInteger(ulong numberOfDigits)
{
    if(numberOfDigits == 0)
    {
        throw new ArgumentException(
           "The number of digits cannot be zero.", "numberOfDigits");
    }

    var digit = new StringBuilder();

    for(var i = 0u; i < numberOfDigits; i++)
    {
        if(i == 0)
        {
            digit.Append(this.Next(1, 10));
        }
        else
        {
            digit.Append(this.Next(10));
        }
    }

    return BigInteger.Parse(digit.ToString(), CultureInfo.CurrentCulture);
}

Using a StringBuilder like that wasn't the smartest thing I've ever done. But my first goal was to get a working implementation in place with tests, and then go back and improve it. I also created a performance test to ensure I could compare apples to apples:

[TestMethod]
public void TimeCreateBigInteger()
{
    const int iterations = 100;

    for(var i = 10u; i <= 100000u; i *= 10u)
    {
        this.TestContext.WriteLine("{0} size, total time is {1}", i, new Action(() =>
        {
            using(var random = new SecureRandom())
            {
                for(var j = 0; j < iterations; j++)
                {
                    random.GetBigInteger(i);
                }
            }
        }).Time());
    }
}

With this test in place, my results would look like this:

10 size, total time is 00:00:00.0091400
100 size, total time is 00:00:00.0492115
1000 size, total time is 00:00:00.5380570
10000 size, total time is 00:00:09.1498312
100000 size, total time is 00:08:35.1854318

My second attempt was to try and reduce the number of times I was in the for loop:

public BigInteger GetBigInteger(ulong numberOfDigits)
{
    if(numberOfDigits == 0)
    {
        throw new ArgumentException(
           "The number of digits cannot be zero.", "numberOfDigits");
    }

    var digit = new StringBuilder();

    if(numberOfDigits == 1)
    {
        digit.Append(this.Next(1, 10));
    }
    else
    {
        var remainingDigits = numberOfDigits;

        while(remainingDigits > 0)
        {
            if(remainingDigits >= 9)
            {
                digit.Append(this.Next(100000000, 1000000000));
                remainingDigits -= 9;
            }
            else
            {
                digit.Append(this.Next((int)(Math.Pow(10, remainingDigits - 1)), 
                    (int)(Math.Pow(10, remainingDigits))));
                remainingDigits = 0;
            }
        }
    }

    return BigInteger.Parse(digit.ToString(), CultureInfo.CurrentCulture);
}

I still didn't remove the StringBuilder, but I thought this would improve things. Was I ever wrong:

10 size, total time is 00:00:00.0076844
100 size, total time is 00:00:00.0107465
1000 size, total time is 00:00:00.1392907
10000 size, total time is 00:00:05.3649017
100000 size, total time is 00:07:36.3556192

This is a perfect illustration why you must have numbers to back up your investigations. I did improve things a bit, but obviously I need to do some more surgery to get things better. Having these numbers in place is a reality check to squash my blind hopes.

My 3rd (and so far final) attempt continues down the path in the 2nd version, but it removes the StringBuilder entirely:

public BigInteger GetBigInteger(ulong numberOfDigits)
{
    if(numberOfDigits == 0)
    {
        throw new ArgumentException(
           "The number of digits cannot be zero.", "numberOfDigits");
    }

    var digit = BigInteger.Zero;

    if(numberOfDigits == 1)
    {
        digit = new BigInteger(this.Next(0, 10));
    }
    else
    {
        var remainingDigits = numberOfDigits;

        while(remainingDigits > 0)
        {
            if(remainingDigits >= 9)
            {
                digit = (1000000000 * digit) + this.Next(100000000, 1000000000);
                remainingDigits -= 9;
            }
            else
            {
                digit = ((int)(Math.Pow(10, remainingDigits)) * digit) + 
                    this.Next((int)(Math.Pow(10, remainingDigits - 1)), 
                        (int)(Math.Pow(10, remainingDigits)));
                remainingDigits = 0;
            }
        }
    }
                    
    return digit;
}

Essentially, I'm just moving existing digits over with the multiplication, and inserting new numbers with the addition.

Now the results look much better:

10 size, total time is 00:00:00.0052317
100 size, total time is 00:00:00.0069716
1000 size, total time is 00:00:00.0539089
10000 size, total time is 00:00:01.0511939
100000 size, total time is 00:00:55.0149691

This is roughly an order of magnitude better. I might be able to make this even better (I already have some ideas in mind), but now I have a baseline test in place to make sure I'm making things better from a performance perspective. A deeper analysis with profiling tools would be advantageous, but this poor man's approach has already paid off.

* Posted at 05.14.2010 10:56:00 AM (Last Update: 06.03.2010 11:28:22 AM) | 2 comments | Link | RSS *

Comments

# byte[], from James Curran at 05.21.2010 11:22:29 AM

My first guess is that a method based on the byte[] ctor of BigInteger will be the fastest (I can only guess since I don't have .NET 4 yet). However, that was based on my assumption that BigInteger was implemented as a byte[] of BCD digits, one per byte. Apparently it's implemented as a standard binary number, with just variable size. So, there's no simple way to generate a hex number that has a specific number of digits when rendered in decimal.

# Take II, from James Curran at 06.03.2010 11:28:21 AM

Given our goal of generating a hex number that has certain properties when rendered as decimal number, my question is "How close do we have to be?"

GetBigInteger(ulong numberOfDigits) will return a random BigInteger number in the range 0 to N where N is

10 ^ numberOfDigits +/- no more than 0.4%

(e.g. for numberOfDigits == 6, the highest number possibly returned is 1000079)


public BigInteger GetBigInteger(ulong numberOfDigits)
{
var log16 = Math.Log(Math.Pow(10, numberOfDigits), 256);
var digits = Math.Floor(log16);
var rem = Math.Round(Math.Pow(256, log16-digits));

var bytes = new byte[digits + 1];
this.NextBytes(bytes);
bytes[0] = (byte) this.Next(rem);

return new BigInteger(bytes);
}

(NOTE: I still don't have .NET v4 so I couldn't actually test that, so there may be an off-by-one error in it.)

(NOTE 2: There is probably a simpler math formula for calculating the log16 value, which would make this even faster)

Add a Comment

(*) = Required field
Name (*):

E-Mail (*):

Web Site:

Title (*):

Comments (*):

Enter the code you see (*)



Quote
"To know that you do not know is the best. To pretend to know when you do not know is disease." Lao Tzu
Twitter History
follow me on Twitter
Blog History