Ich habe meine eigene Analyse einiger der Algorithmen in diesem Thread durchgeführt und bin dabei zu einigen neuen Ergebnissen gekommen. Sie können diese alten Ergebnisse in der Bearbeitungshistorie dieser Antwort sehen, aber sie sind nicht korrekt, da ich einen Fehler gemacht und Zeit damit verschwendet habe, mehrere Algorithmen zu analysieren, die nicht annähernd gleich sind. Ich habe jedoch die Lehren aus verschiedenen Antworten gezogen und habe jetzt zwei Algorithmen, die den "Gewinner" dieses Threads vernichten. Hier ist der Kernpunkt, den ich anders mache als alle anderen:
// This is faster because a number is divisible by 2^4 or more only 6% of the time
// and more than that a vanishingly small percentage.
while((x & 0x3) == 0) x >>= 2;
// This is effectively the same as the switch-case statement used in the original
// answer.
if((x & 0x7) != 1) return false;
Diese einfache Zeile, die in den meisten Fällen eine oder zwei sehr schnelle Anweisungen hinzufügt, vereinfacht jedoch die switch-case
Anweisung zu einer if-Anweisung zusammenfassen. Dies kann jedoch die Laufzeit verlängern, wenn viele der getesteten Zahlen signifikante Zweierpotenzen haben.
Die folgenden Algorithmen sind wie folgt:
- Internet - Kip's gepostete Antwort
- Durron - Meine modifizierte Antwort auf der Grundlage der Antwort in einem Durchgang
- DurronTwo - Meine modifizierte Antwort unter Verwendung der Two-Pass-Antwort (von @JohnnyHeggheim), mit einigen anderen leichten Änderungen.
Hier ist ein Beispiel für die Laufzeit, wenn die Zahlen mit Math.abs(java.util.Random.nextLong())
0% Scenario{vm=java, trial=0, benchmark=Internet} 39673.40 ns; ?=378.78 ns @ 3 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 37785.75 ns; ?=478.86 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronTwo} 35978.10 ns; ?=734.10 ns @ 10 trials
benchmark us linear runtime
Internet 39.7 ==============================
Durron 37.8 ============================
DurronTwo 36.0 ===========================
vm: java
trial: 0
Und hier ist ein Beispiel für die Laufzeit, wenn es nur für die erste Million Longs ausgeführt wird:
0% Scenario{vm=java, trial=0, benchmark=Internet} 2933380.84 ns; ?=56939.84 ns @ 10 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 2243266.81 ns; ?=50537.62 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronTwo} 3159227.68 ns; ?=10766.22 ns @ 3 trials
benchmark ms linear runtime
Internet 2.93 ===========================
Durron 2.24 =====================
DurronTwo 3.16 ==============================
vm: java
trial: 0
Wie Sie sehen können, DurronTwo
schneidet bei großen Eingaben besser ab, weil er den Zaubertrick sehr oft anwenden kann, wird aber im Vergleich zum ersten Algorithmus und Math.sqrt
weil die Zahlen so viel kleiner sind. Inzwischen ist die einfachere Durron
ist ein riesiger Gewinner, weil er in der ersten Million Zahlen nicht viele Male durch 4 teilen muss.
Hier ist Durron
:
public final static boolean isPerfectSquareDurron(long n) {
if(n < 0) return false;
if(n == 0) return true;
long x = n;
// This is faster because a number is divisible by 16 only 6% of the time
// and more than that a vanishingly small percentage.
while((x & 0x3) == 0) x >>= 2;
// This is effectively the same as the switch-case statement used in the original
// answer.
if((x & 0x7) == 1) {
long sqrt;
if(x < 410881L)
{
int i;
float x2, y;
x2 = x * 0.5F;
y = x;
i = Float.floatToRawIntBits(y);
i = 0x5f3759df - ( i >> 1 );
y = Float.intBitsToFloat(i);
y = y * ( 1.5F - ( x2 * y * y ) );
sqrt = (long)(1.0F/y);
} else {
sqrt = (long) Math.sqrt(x);
}
return sqrt*sqrt == x;
}
return false;
}
Und DurronTwo
public final static boolean isPerfectSquareDurronTwo(long n) {
if(n < 0) return false;
// Needed to prevent infinite loop
if(n == 0) return true;
long x = n;
while((x & 0x3) == 0) x >>= 2;
if((x & 0x7) == 1) {
long sqrt;
if (x < 41529141369L) {
int i;
float x2, y;
x2 = x * 0.5F;
y = x;
i = Float.floatToRawIntBits(y);
//using the magic number from
//http://www.lomont.org/Math/Papers/2003/InvSqrt.pdf
//since it more accurate
i = 0x5f375a86 - (i >> 1);
y = Float.intBitsToFloat(i);
y = y * (1.5F - (x2 * y * y));
y = y * (1.5F - (x2 * y * y)); //Newton iteration, more accurate
sqrt = (long) ((1.0F/y) + 0.2);
} else {
//Carmack hack gives incorrect answer for n >= 41529141369.
sqrt = (long) Math.sqrt(x);
}
return sqrt*sqrt == x;
}
return false;
}
Und mein Benchmark-Kabelbaum: (Benötigt Google caliper 0.1-rc5)
public class SquareRootBenchmark {
public static class Benchmark1 extends SimpleBenchmark {
private static final int ARRAY_SIZE = 10000;
long[] trials = new long[ARRAY_SIZE];
@Override
protected void setUp() throws Exception {
Random r = new Random();
for (int i = 0; i < ARRAY_SIZE; i++) {
trials[i] = Math.abs(r.nextLong());
}
}
public int timeInternet(int reps) {
int trues = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < ARRAY_SIZE; j++) {
if(SquareRootAlgs.isPerfectSquareInternet(trials[j])) trues++;
}
}
return trues;
}
public int timeDurron(int reps) {
int trues = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < ARRAY_SIZE; j++) {
if(SquareRootAlgs.isPerfectSquareDurron(trials[j])) trues++;
}
}
return trues;
}
public int timeDurronTwo(int reps) {
int trues = 0;
for(int i = 0; i < reps; i++) {
for(int j = 0; j < ARRAY_SIZE; j++) {
if(SquareRootAlgs.isPerfectSquareDurronTwo(trials[j])) trues++;
}
}
return trues;
}
}
public static void main(String... args) {
Runner.main(Benchmark1.class, args);
}
}
UPDATE: Ich habe einen neuen Algorithmus entwickelt, der in einigen Szenarien schneller und in anderen langsamer ist, und ich habe verschiedene Benchmarks auf der Grundlage verschiedener Eingaben erhalten. Wenn wir modulo berechnen 0xFFFFFF = 3 x 3 x 5 x 7 x 13 x 17 x 241
können wir 97,82 % der Zahlen eliminieren, die keine Quadrate sein können. Dies lässt sich (quasi) in einer Zeile mit 5 bitweisen Operationen bewerkstelligen:
if (!goodLookupSquares[(int) ((n & 0xFFFFFFl) + ((n >> 24) & 0xFFFFFFl) + (n >> 48))]) return false;
Der resultierende Index ist entweder 1) der Rückstand, 2) der Rückstand + 0xFFFFFF
oder 3) der Rückstand + 0x1FFFFFE
. Natürlich brauchen wir eine Nachschlagetabelle für Reste modulo 0xFFFFFF
die etwa 3 MB groß ist (in diesem Fall als ascii-Text-Dezimalzahlen gespeichert, nicht optimal, aber mit einem entsprechenden Programm deutlich verbesserungsfähig). ByteBuffer
und so weiter. Aber da es sich um eine Vorberechnung handelt, ist das nicht so wichtig. Sie können die Datei hier finden (oder erstellen Sie sie selbst):
public final static boolean isPerfectSquareDurronThree(long n) {
if(n < 0) return false;
if(n == 0) return true;
long x = n;
while((x & 0x3) == 0) x >>= 2;
if((x & 0x7) == 1) {
if (!goodLookupSquares[(int) ((n & 0xFFFFFFl) + ((n >> 24) & 0xFFFFFFl) + (n >> 48))]) return false;
long sqrt;
if(x < 410881L)
{
int i;
float x2, y;
x2 = x * 0.5F;
y = x;
i = Float.floatToRawIntBits(y);
i = 0x5f3759df - ( i >> 1 );
y = Float.intBitsToFloat(i);
y = y * ( 1.5F - ( x2 * y * y ) );
sqrt = (long)(1.0F/y);
} else {
sqrt = (long) Math.sqrt(x);
}
return sqrt*sqrt == x;
}
return false;
}
Ich lade sie in eine boolean
Array wie dieses:
private static boolean[] goodLookupSquares = null;
public static void initGoodLookupSquares() throws Exception {
Scanner s = new Scanner(new File("24residues_squares.txt"));
goodLookupSquares = new boolean[0x1FFFFFE];
while(s.hasNextLine()) {
int residue = Integer.valueOf(s.nextLine());
goodLookupSquares[residue] = true;
goodLookupSquares[residue + 0xFFFFFF] = true;
goodLookupSquares[residue + 0x1FFFFFE] = true;
}
s.close();
}
Beispiel-Laufzeit. Es schlägt Durron
(Version eins) in jedem Versuch, den ich durchgeführt habe.
0% Scenario{vm=java, trial=0, benchmark=Internet} 40665.77 ns; ?=566.71 ns @ 10 trials
33% Scenario{vm=java, trial=0, benchmark=Durron} 38397.60 ns; ?=784.30 ns @ 10 trials
67% Scenario{vm=java, trial=0, benchmark=DurronThree} 36171.46 ns; ?=693.02 ns @ 10 trials
benchmark us linear runtime
Internet 40.7 ==============================
Durron 38.4 ============================
DurronThree 36.2 ==========================
vm: java
trial: 0
0 Stimmen
Da Integer und Long in den meisten c-ähnlichen Sprachen keine spezifische Länge haben (was Ihr Code zu sein scheint), ist es besser zu sagen, dass es für ein 32-Bit-Integer 2**16 perfekte Quadrate gibt.
28 Stimmen
Dies ist Java-Code, bei dem int==32 Bits und long==64 Bits sind, und beide sind vorzeichenbehaftet.
0 Stimmen
Welches ist schneller: "long tst = (long)Math.sqrt(n); return tst*tst == n;" (was du hast) oder "double tst = Math.sqrt(n); return tst ==(double)Math.round(tst);" ?
0 Stimmen
Welche JVM haben Sie für das Testen verwendet? In meiner Erfahrung hängt die Leistung des Algorithmus von der JVM ab.
16 Stimmen
@Shreevasta: Ich habe einige Tests mit großen Werten (größer als 2^53) durchgeführt und deine Methode liefert einige falsche positive Ergebnisse. Das erste, das auftritt, ist für n=9007199326062755, das keine perfekte Quadratzahl ist, aber als solche zurückgegeben wird.
0 Stimmen
Es gibt einen Fehler in Ihrem Code: sqrt = (long)Math.sqrt(n); return sqrt*sqrt == n; Math.sqrt(n) gibt eine Gleitkommazahlenrepräsentation, z.B. Math.sqrt(9) könnte 2,99999999999 zurückgeben, wenn Sie Pech haben, und wenn Sie es mit (long) casten, könnten Sie leicht eine zu niedrige Zahl haben.
0 Stimmen
Sie können Bittricks verwenden, um die letzten 6 Bits zu überprüfen: if( (x&2) || ((x & 7) == 5) || ((x & 11) == 8) || ((x & 31) == 20) || ((x & 47) == 24)) return false;
39 Stimmen
Bitte nennen Sie es nicht den "John Carmack Hack". Er hat es nicht erfunden.
1 Stimmen
@martinus: Für Werte unter 2^53 ist die doppelte Darstellung genau, sodass es keinen Rundungsfehler gibt. Ich habe auch bei jedem vollkommenen Quadrat größer als 2^53 getestet, sowie +/- 1 von jedem vollkommenen Quadrat, und Rundungsfehler führen nie zu einer falschen Antwort.
0 Stimmen
Ich glaube, dass moderne JVMs möglicherweise in der Lage sind, Array-Indexprüfungen an einer bestimmten Stelle zu überspringen, wenn daraus geschlossen werden kann, dass sie dort immer gültig sein werden. Mit welcher JVM wurde der Test durchgeführt?
0 Stimmen
Der "John Carmack-Hack" sollte für einen größeren Bereich möglich sein, indem die zusätzliche Durchlaufdurchführung im Quellcode von Quake auskommentiert wird, wenn nötig (d.h. eine große genug Zahl).
0 Stimmen
@Thorbjørn Ravn Andersen: Ich habe den J2SE 6.0 Windows JVM verwendet, den ich von der Website von Sun heruntergeladen habe. Ich habe auch versucht, die zusätzliche Iteration zu kommentieren, und wenn ich mich richtig erinnere, wurde es auf seltsame Weise weniger genau.
0 Stimmen
Machen Sie Ihren Quickfail schneller und machen // Quickfail if( n < 0 || ((n&2) != 0) || ((n & 7) == 5) || ((n & 11) == 8) ) return false; if( n == 0 ) return true; andersherum: // Quickfail if( n == 0 ) return true; if( n < 0 || ((n&2) != 0) || ((n & 7) == 5) || ((n & 11) == 8) ) return false;
0 Stimmen
@dstibbe: Das würde nur schneller sein, wenn die Eingabe 0 ist. Für 75% der anderen Eingaben (ohne negative Zahlen zu zählen) wird es schneller sein, wie es geschrieben ist, und für die anderen 25% wird es keinen Unterschied geben.
98 Stimmen
@mamama - Vielleicht, aber ihm wird das zugeschrieben. Henry Ford erfand nicht das Auto, die Wright Brüder erfanden nicht das Flugzeug, und Galileo war nicht der erste, der herausfand, dass sich die Erde um die Sonne drehte... Die Welt besteht aus gestohlenen Erfindungen (und Liebe).
0 Stimmen
@Kip, der Mikrobenchmark für den Algorithmus könnte wegen der relativ großen Tabelle ungenau sein. Wenn die gesamte Tabelle im Cache ist (enge Schleifen), wird es keine Cache-Misses geben. Das boolean[] sollte durch lange Konstanten (oder ein Array) ersetzt werden und wird dadurch etwas schneller.
4 Stimmen
Sie könnten eine geringfügige Geschwindigkeitssteigerung im 'quickfail' erzielen, wenn Sie etwas wie
((1<<(n&15))|65004) != 0
verwenden, anstatt drei separate Überprüfungen zu haben.1 Stimmen
Warum fügst du das 0.5 hinzu?
1 Stimmen
@KorayTugay, um die Zahl zu runden. Mein Bedenken war, dass Math.sqrt einen Wert zurückgeben könnte, der aufgrund von Rundungsfehlern leicht abweicht. Angenommen, wenn
math.sqrt(100)
9.999999999999999
anstelle von10
zurückgeben würde. Ich bin mir jedoch nicht sicher, ob es tatsächlich Fälle gibt, in denen dies passiert.0 Stimmen
@Kip schau dir meine Antwort an, ich habe einen signifikanten Leistungsgewinn auf deine gepostete Antwort erzielt.
0 Stimmen
@Kip Ich habe meinen Beitrag bearbeitet, um einen dritten Algorithmus hinzuzufügen, der manchmal besser abschneidet als meine vorherige Antwort.
1 Stimmen
Überraschenderweise habe ich auf eine so beliebte Frage niemanden hingewiesen gesehen, dass man bei höherem
n
genauer sein kann, indem man eine weitere Iteration durchführt, die als 'Carmack-Hack' bezeichnet wird. Hinweis: Das Ergebnis stammt nicht von Carmack, sondern von Newton/Raphson. Ich vermute, dass der 'Magische Zahl'-Hack eine fairere Zuschreibung an Carmack wäre.1 Stimmen
Nicht sicher, ob dies das ist, was du willst, aber das dauert etwa 1 Millisekunde. (JavaScript, nicht Java.)
Rückgabe math.sqrt(x).split(".").length > 1
0 Stimmen
Ich bin spät zur Party gekommen, aber ich glaube, du könntest noch mehr Leistung aus deinem endgültigen Code-Schnipsel herausholen, indem du Techniken von hier verwendest: stackoverflow.com/questions/15621083/…
1 Stimmen
@user3932000 Ich habe das gemacht, weil es zu einer leichten Compiler-Optimierung führen kann. Hier ist eine bessere Diskussion: stackoverflow.com/questions/5547663/…
1 Stimmen
Ich bin noch nie auf ein PE-Problem gestoßen, das nicht in unter einer Minute in Ruby berechnet werden könnte. Daher ist das Argument, dass die Leistung für dieses Ergebnis wichtig ist, ein wenig unglaublich.
0 Stimmen
Ich bin gespannt, wie sich diese Weisheit angesichts von Innovationen wie SSE SQRTPS bewährt. felixcloutier.com/x86/SQRTPS.html,
0 Stimmen
Was ist ein Beispiel für ein Problem, bei dem das einen Unterschied macht? Anstatt einzelne Quadratwurzeln für eine lange Liste zu berechnen, ist es viel besser, die Liste der perfekten Quadrate abzuarbeiten und die Liste auf diese Weise zu filtern.
1 Stimmen
Für sehr große Zahlen gibt es einen schnellen randomisierten Algorithmus: petr-mitrichev.blogspot.com/2017/12/a-quadratic-week.html
2 Stimmen
@RobertFraser Galileo Galilei war nicht der Erste, dessen Name verstümmelt wurde.
1 Stimmen
@RobertFraser: Ja, und das sind große Ungerechtigkeiten, gegen die du nichts unternehmen kannst. Dies ist niemals eine Rechtfertigung für dein schlechtes Verhalten.
0 Stimmen
Ich würde vorschlagen, einen Integer-Quadratwurzelalgorithmus zu verwenden stackoverflow.com/questions/1100090
2 Stimmen
@RobertFraser Ich habe noch nie gehört, dass jemand behauptet hat, dass Galileo das heliozentrische Modell erfunden hat. Es wird immer Kopernikus zugeschrieben. Meintest du ihn?