3
\$\begingroup\$

(See the previous iteration.)

My two previous methods for computing the integer square root of a number \$N\$ ran in the \$\mathcal{O}(\sqrt{N})\$ worst case time. Now I have added a method (intSqrt3) that runs in \$\mathcal{O}(\log \sqrt{N})\$ time:

Main.java:

import java.util.Random;
import java.util.function.Function;
public class Main {
 public static long intSqrt1(long number) {
 long sqrt = 0L;
 while ((sqrt + 1) * (sqrt + 1) <= number) {
 sqrt++;
 }
 return sqrt;
 }
 public static long intSqrt2(long number) {
 if (number <= 0L) {
 return 0L;
 }
 long sqrt = 1L;
 while (4 * sqrt * sqrt <= number) {
 sqrt *= 2;
 }
 while ((sqrt + 1) * (sqrt + 1) <= number) {
 sqrt++;
 }
 return sqrt;
 }
 public static long intSqrt3(long number) {
 if (number <= 0L) {
 return 0L;
 }
 long sqrt = 1L;
 // Do the exponential search.
 while (4 * sqrt * sqrt <= number) {
 sqrt *= 2;
 }
 long left = sqrt;
 long right = 2 * sqrt;
 long middle = 0;
 // Do the binary search over the range that is guaranteed to contain 
 // the integer square root.
 while (left < right) {
 middle = left + (right - left) / 2;
 if (middle * middle < number) {
 left = middle + 1;
 } else if (middle * middle > number) {
 right = middle - 1;
 } else {
 return middle;
 }
 }
 // Correct the binary search "noise". This iterates no more than 3
 // times.
 long ret = middle + 1;
 while (ret * ret > number) {
 --ret;
 }
 return ret; 
 }
 public static long intSqrt4(long number) {
 return (long) Math.sqrt(number);
 }
 private static void profile(Function<Long, Long> function, Long number) {
 long result = 0L;
 long startTime = System.nanoTime();
 for (int i = 0; i < ITERATIONS; ++i) {
 result = function.apply(number);
 }
 long endTime = System.nanoTime();
 System.out.printf("Time: %.2f, result: %d.\n", 
 (endTime - startTime) / 1e6,
 result);
 }
 private static final int ITERATIONS = 1_000;
 private static final long UPPER_BOUND = 1_000_000_000_000L;
 public static void main(String[] args) {
 long seed = System.nanoTime();
 Random random = new Random(seed);
 long number = Math.abs(random.nextLong()) % UPPER_BOUND;
 System.out.println("Seed = " + seed);
 System.out.println("Number: " + number);
 profile(Main::intSqrt1, number);
 profile(Main::intSqrt2, number);
 profile(Main::intSqrt3, number);
 profile(Main::intSqrt4, number);
 }
}

The performance figures I get looks like this:

Seed = 19608492647714
Number: 54383384696
Time: 531.18, result: 233202.
Time: 218.41, result: 233202.
Time: 1.81, result: 233202.
Time: 0.43, result: 233202.

Above, intSqrt3 took 1.81 milliseconds.

Critique request

Is there something I could improve? Naming/coding conventions? Performance? API design?

asked Jan 17, 2016 at 6:54
\$\endgroup\$
3
  • 2
    \$\begingroup\$ Never, ever, believe in results of sub-second timing runs. \$\endgroup\$ Commented Jan 17, 2016 at 11:02
  • \$\begingroup\$ I would try to come up with better names for some variables. What is ret? \$\endgroup\$ Commented Jan 17, 2016 at 12:53
  • \$\begingroup\$ You might be interested in a certain MicroBench library. \$\endgroup\$ Commented Jan 19, 2016 at 15:47

4 Answers 4

5
\$\begingroup\$

When using function parameters, use the primitive types when available:

Function<Long, Long> function

is a red-flag, and should be LongUnaryOperator.

Your code will spin in to an infinite loop for 25% of all long values .... anything larger than Long.MAX_VALUE/4 will cause this loop to become infinite:

 // Do the exponential search.
 while (4 * sqrt * sqrt <= number) {
 sqrt *= 2;
 }

About that loop.... why do you have a magic number 4....? What does it do?

This code needs more testing... and magic numbers need to be removed.

answered Jan 17, 2016 at 13:33
\$\endgroup\$
1
  • \$\begingroup\$ The constant 4 makes sure that multiplying the current "guess" by a factor of 2 will not exceed the square root of number. And no, I did not expect that routine to be flawless. \$\endgroup\$ Commented Jan 17, 2016 at 13:39
1
\$\begingroup\$

You want something fast and efficient.

But did you really check what this method does :

public static long intSqrt1(long number) {
 long sqrt = 0L;
 while ((sqrt + 1) * (sqrt + 1) <= number) {
 sqrt++;
 }
 return sqrt;
}

Your adding 1 to sqrt 3 times.

I don't see any reason why you should do that, but I'm guessing it's for the easy part for returning sqrt.

Let's refactor just this to a more efficient method.

First of all, a while loop where you need to count your steps, that's called a for loop.

public static long intSqrt1(long number) {
 long sqrt;
 for (sqrt = 1; (sqrt * sqrt) <= number; sqrt++) {}
 return --sqrt;
}

This method is doing all the same but I do raise the sqrt only once each time and if I return it, I will decrease it.

Now I did write some basic test, it's not a how a real performance test should be but in this case you will see the difference because it's big :

public static void main(String[] args) {
 long startTime = System.nanoTime();
 for (int i = 0; i < 10000; i++) {
 intSqrt1(902545489); // new one
 }
 long midTime = System.nanoTime();
 for (int j = 0; j < 10000; j++) {
 intSqrt2(902545489); // old one
 }
 long endTime = System.nanoTime();
 System.out.println((midTime - startTime) + " vs " + (endTime - midTime));
}

As you can see, the for I initialize 2 time a new integer and I put the new method first because we can have a delay with the startup so there could be time faults in the first method.

Still I got this as output: (I put dot's for easy reading)

175.509.799 vs 360.087.176

As you can see I halved the time.

rolfl
98.1k17 gold badges219 silver badges419 bronze badges
answered Jan 18, 2016 at 7:06
\$\endgroup\$
2
  • \$\begingroup\$ return sqrt - 1; looks better I think (no need for assignation) \$\endgroup\$ Commented Jan 18, 2016 at 14:39
  • \$\begingroup\$ For me that's personal flavour, It's possible that - 1 is a lit faster but I don't know that. \$\endgroup\$ Commented Jan 18, 2016 at 18:14
0
\$\begingroup\$

Since you know Long.MAX_VALUE in advance, you can hard-code it's square root. You can then perform a binary search between 1 and this pre-computed maximum.

It will remove the questionable "exponential search".

You will then also achieve a \$\mathcal{O}(\log \sqrt{Long.MAX\_VALUE})\$ complexity, which is actually \$\mathcal{O}(1)\$ as observed by @Simon Forsberg. This means that the execution duration can be bounded by a constant time (this does not necessary means that this algorithm is the fastest).

answered Jan 19, 2016 at 15:41
\$\endgroup\$
4
  • 2
    \$\begingroup\$ I'd rather say that this will be O(log(sqrt(CONSTANT)), the complexity does not scale according to the input in this case. Technically, because sqrt(CONSTANT) is also a constant, and log(constant) is also a constant, you could even call this O(1). \$\endgroup\$ Commented Jan 19, 2016 at 15:51
  • \$\begingroup\$ @SimonForsberg that's right, I updated my answer accordingly \$\endgroup\$ Commented Jan 19, 2016 at 16:46
  • \$\begingroup\$ Since the input is bounded, all algorithms are actually O(1) (it might just not be trivial to find the bound) \$\endgroup\$ Commented Jan 19, 2016 at 16:47
  • \$\begingroup\$ I think we proved that P = NP. Money, here we go! \$\endgroup\$ Commented Jan 26, 2016 at 18:43
0
\$\begingroup\$

For the hell of micro-benchmarking without the likes of Java Microbenchmarking Harness or jmicrobench(no idea whether this is official) (or that most visible one for those who don't have issues with empires), I tinkered around picking up ideas from rolfl and chillworld

private static void profile(UnaryLongFunc function, Random r) {
 long
 result = 0L,
 iterations = function.count,
 startTime = System.nanoTime();
 StringBuilder results = new StringBuilder(123);
 for (long i = 0; i < iterations; ++i) {
 long t = r.nextLong() % UPPER_BOUND;
 result = function.apply(t < 0 ? t + UPPER_BOUND : t);
 if (i < 7)
 results.append(", ").append(result);
 }
 long endTime = System.nanoTime();
 System.out.printf("%-12s %11.2f%s\n", function.label,
 (double)(endTime - startTime) / iterations, results);
}
// private static final int ITERATIONS = 1_000;
private static final long UPPER_BOUND = 1_000_000_000_000L;//Long.MAX_VALUE;
/** increment sqrt just once per iteration (chillworld) */
public static long intSqrt11(long number) {
 long sqrt;
 for (sqrt = 1; (sqrt * sqrt) <= number; sqrt++) {}
 return --sqrt;
}
/** source-level strength reduction */
public static long intSqrt12(long number) {
 for (long sq = 0, inc = 1 ; ; sq += inc, inc += 2)
 if (number <= sq)
 return (inc >>> 1) - 1;
}
/** int for increment & square up to Integer.MAX_VALUE */
static int SQUARE_LIMIT = (Integer.MAX_VALUE-Character.MAX_VALUE)>>>1;
public static long intSqrt13(long number) {
 int sq = 0,
 limit = (int) Math.min(SQUARE_LIMIT, number),
 inc = 1;
 while (sq < limit) {
 sq += inc;
 inc += 2;
 }
 if (number <= sq)
 return (inc >>> 1) - 1;
 long lsq = sq, linc = inc;
 while (lsq < number) {
 lsq += linc;
 linc += 2;
 }
 return (linc >>> 1) - 1;
}
/** int for increment for number below Integer.MAX_VALUE**2
 * & for square up to Integer.MAX_VALUE */
public static long intSqrt14(long number) {
 int sq = 0,
 limit = (int) Math.min(SQUARE_LIMIT, number),
 inc = 1;
 while (sq < limit) {
 sq += inc;
 inc += 2;
 }
 if (number <= sq)
 return (inc >>> 1) - 1;
 long lsq = sq,
 longLimit = Math.min((Integer.MAX_VALUE-1L) * Integer.MAX_VALUE, number);
 while (lsq < longLimit) {
 lsq += inc;
 inc += 2;
 }
 if (number <= lsq)
 return (inc >>> 1) - 1;
 long linc = inc;
 while (lsq < number) {
 lsq += linc;
 linc += 2;
 }
 return (linc >>> 1) - 1;
}
static abstract class UnaryLongFunc { // imitates LongUnaryOperator
 final String label;
 long count;
 UnaryLongFunc(String label, long callCount) {
 this.label = label;
 count = callCount;
 }
 abstract long apply(long to);
}
static UnaryLongFunc return1;
static UnaryLongFunc []candy = {
 new UnaryLongFunc("Sqrt1", 5000) {
 @Override long apply(long to) { return intSqrt1(to); }
 },
 new UnaryLongFunc("Sqrt11", 10000) {
 @Override long apply(long to) { return intSqrt11(to); }
 },
 new UnaryLongFunc("Sqrt12", 10000) {
 @Override long apply(long to) { return intSqrt12(to); }
 },
 new UnaryLongFunc("Sqrt13", 10000) {
 @Override long apply(long to) { return intSqrt13(to); }
 },
 new UnaryLongFunc("Sqrt14", 10000) {
 @Override long apply(long to) { return intSqrt14(to); }
 },
 new UnaryLongFunc("Sqrt2", 10000) {
 @Override long apply(long to) { return intSqrt2(to); }
 },
 new UnaryLongFunc("Sqrt3", 20000000) {
 @Override long apply(long to) { return intSqrt3(to); }
 },
 new UnaryLongFunc("Sqrt4", 100000000L) {
 @Override long apply(long to) { return intSqrt4(to); }
 },
 // one call overhead less
 return1 = new UnaryLongFunc("return 1", 1) {
 @Override long apply(long to) { return 0; }
 }
 };
public static void main(String[] args) {
 long seed = System.nanoTime();
 System.out.println("Seed = " + seed);
 for (long count = 1000 ; count <= 10000000L ; count *= 10) {
 return1.count = count;
 profile(return1, new Random(seed));
 }
 System.out.println("warmup ...");
 long total = 0;
 for (int i = Integer.MAX_VALUE / 500 ; 0 <= --i ; )
 for (long l = 42 ; 0 < l ; l >>= 1)
 for (UnaryLongFunc luo: candy)
 total += luo.apply(l);
 System.out.println(total);
 System.gc();
 for (int cd = 5 ; 0 <= --cd ; )
 profile(return1, new Random(seed));
 System.out.println(total);
 for (int cc = candy.length, ulf = cc, change = -1 ; (ulf += change) < cc ; ) {
 System.gc();
 profile(candy[ulf], new Random(seed));
 if (ulf <= 0)
 change = 1;
 }
}

In my environment, warmup of the empty/non-method made a ratio in execution time of about 100:1.

community wiki

\$\endgroup\$

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.