java - round - que es el math floor
Java 8u40 Math.round() muy lento (3)
Evaluación comparativa informal : usted evalúa A, pero en realidad mide B y concluye que ha medido C.
Las JVM modernas son demasiado complejas y hacen todo tipo de optimización. Si intenta medir un pequeño fragmento de código, es realmente complicado hacerlo correctamente sin un conocimiento muy, muy detallado de lo que está haciendo la JVM. El culpable de muchos puntos de referencia es la eliminación del código muerto: los compiladores son lo suficientemente inteligentes como para deducir que algunos cálculos son redundantes, y eliminarlos por completo. Por favor lea las siguientes diapositivas http://shipilev.net/talks/jvmls-July2014-benchmarking.pdf . Para "arreglar" la marca microbiológica de Adam (todavía no puedo entender lo que está midiendo y esta "solución" no tiene en cuenta el calentamiento, la OSR y muchas otras dificultades de la marca microbiológica) tenemos que imprimir el resultado del cálculo al sistema. salida:
int result = 0;
long t0 = System.currentTimeMillis();
for (int i = 0; i < 1e9; i++) {
result += Math.round((float) i / (float) (i + 1));
}
long t1 = System.currentTimeMillis();
System.out.println("result = " + result);
System.out.println(String.format("%s, Math.round(float), %.1f ms", System.getProperty("java.version"), (t1 - t0)/1f));
Como resultado:
result = 999999999
1.8.0_25, Math.round(float), 5251.0 ms
result = 999999999
1.8.0_40, Math.round(float), 3903.0 ms
El mismo "arreglo" para el ejemplo MVCE original
It took 401772 milliseconds to complete edu.jvm.runtime.RoundFloatToInt. <==== 1.8.0_40
It took 410767 milliseconds to complete edu.jvm.runtime.RoundFloatToInt. <==== 1.8.0_25
Si desea medir el costo real de Matemáticas # ronda, debe escribir algo como esto (basado en jmh )
package org.openjdk.jmh.samples;
import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;
import org.openjdk.jmh.runner.options.VerboseMode;
import java.util.Random;
import java.util.concurrent.TimeUnit;
@State(Scope.Benchmark)
@BenchmarkMode(Mode.AverageTime)
@OutputTimeUnit(TimeUnit.NANOSECONDS)
@Warmup(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
@Measurement(iterations = 3, time = 5, timeUnit = TimeUnit.SECONDS)
public class RoundBench {
float[] floats;
int i;
@Setup
public void initI() {
Random random = new Random(0xDEAD_BEEF);
floats = new float[8096];
for (int i = 0; i < floats.length; i++) {
floats[i] = random.nextFloat();
}
}
@Benchmark
public float baseline() {
i++;
i = i & 0xFFFFFF00;
return floats[i];
}
@Benchmark
public int round() {
i++;
i = i & 0xFFFFFF00;
return Math.round(floats[i]);
}
public static void main(String[] args) throws RunnerException {
Options options = new OptionsBuilder()
.include(RoundBench.class.getName())
.build();
new Runner(options).run();
}
}
Mis resultados son:
1.8.0_25
Benchmark Mode Cnt Score Error Units
RoundBench.baseline avgt 6 2.565 ± 0.028 ns/op
RoundBench.round avgt 6 4.459 ± 0.065 ns/op
1.8.0_40
Benchmark Mode Cnt Score Error Units
RoundBench.baseline avgt 6 2.589 ± 0.045 ns/op
RoundBench.round avgt 6 4.588 ± 0.182 ns/op
Para encontrar la causa raíz del problema, puede usar github.com/AdoptOpenJDK/jitwatch . Para ahorrar tiempo, puedo decir que el tamaño del código JIT para la ronda de Matemáticas # se incrementó en 8.0_40. Es casi imperceptible para los métodos pequeños, pero en el caso de métodos enormes, una hoja demasiado larga de código de máquina contamina la memoria caché de instrucciones.
Tengo un proyecto de pasatiempo bastante simple escrito en Java 8 que hace uso extensivo de llamadas Math.round () repetidas en uno de sus modos de operación. Por ejemplo, uno de estos modos genera 4 subprocesos y pone en cola 48 tareas ejecutables por medio de un Servicio de ejecución, cada uno de los cuales ejecuta algo similar al siguiente bloque de código 2 ^ 31 veces:
int3 = Math.round(float1 + float2);
int3 = Math.round(float1 * float2);
int3 = Math.round(float1 / float2);
Eso no es exactamente lo que es (hay matrices involucradas y bucles anidados), pero entiendes la idea. De todos modos, antes de Java 8u40, el código que se parece a lo anterior podría completar la ejecución completa de ~ 103 mil millones de bloques de instrucciones en aproximadamente 13 segundos en un AMD A10-7700k. Con Java 8u40 se tarda unos 260 segundos en hacer lo mismo. No hay cambios en el código, no hay nada, solo una actualización de Java.
¿Alguien más ha notado que Math.round () se está volviendo mucho más lento, especialmente cuando se usa repetidamente? Es casi como si la JVM estuviera haciendo algún tipo de optimización antes de que ya no lo esté haciendo. ¿Tal vez estaba usando SIMD antes de 8u40 y no lo es ahora?
Edición: he completado mi segundo intento de un MVCE. Puedes descargar el primer intento aquí:
https://www.dropbox.com/s/rm2ftcv8y6ye1bi/MathRoundMVCE.zip?dl=0
El segundo intento está abajo. Mi primer intento se eliminó de esta publicación, ya que se considera que es demasiado largo y es propenso a las optimizaciones de eliminación de código muerto por parte de la JVM (que aparentemente ocurren menos en 8u40).
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MathRoundMVCE
{
static long grandtotal = 0;
static long sumtotal = 0;
static float[] float4 = new float[128];
static float[] float5 = new float[128];
static int[] int6 = new int[128];
static int[] int7 = new int[128];
static int[] int8 = new int[128];
static long[] longarray = new long[480];
final static int mil = 1000000;
public static void main(String[] args)
{
initmainarrays();
OmniCode omni = new OmniCode();
grandtotal = omni.runloops() / mil;
System.out.println("Total sum of operations is " + sumtotal);
System.out.println("Total execution time is " + grandtotal + " milliseconds");
}
public static long siftarray(long[] larray)
{
long topnum = 0;
long tempnum = 0;
for (short i = 0; i < larray.length; i++)
{
tempnum = larray[i];
if (tempnum > 0)
{
topnum += tempnum;
}
}
topnum = topnum / Runtime.getRuntime().availableProcessors();
return topnum;
}
public static void initmainarrays()
{
int k = 0;
do
{
float4[k] = (float)(Math.random() * 12) + 1f;
float5[k] = (float)(Math.random() * 12) + 1f;
int6[k] = 0;
k++;
}
while (k < 128);
}
}
class OmniCode extends Thread
{
volatile long totaltime = 0;
final int standard = 16777216;
final int warmup = 200000;
byte threads = 0;
public long runloops()
{
this.setPriority(MIN_PRIORITY);
threads = (byte)Runtime.getRuntime().availableProcessors();
ExecutorService executor = Executors.newFixedThreadPool(threads);
for (short j = 0; j < 48; j++)
{
executor.execute(new RoundFloatToIntAlternate(warmup, (byte)j));
}
executor.shutdown();
while (!executor.isTerminated())
{
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
//Do nothing
}
}
executor = Executors.newFixedThreadPool(threads);
for (short j = 0; j < 48; j++)
{
executor.execute(new RoundFloatToIntAlternate(standard, (byte)j));
}
executor.shutdown();
while (!executor.isTerminated())
{
try
{
Thread.sleep(100);
}
catch (InterruptedException e)
{
//Do nothing
}
}
totaltime = MathRoundMVCE.siftarray(MathRoundMVCE.longarray);
executor = null;
Runtime.getRuntime().gc();
return totaltime;
}
}
class RoundFloatToIntAlternate extends Thread
{
int i = 0;
int j = 0;
int int3 = 0;
int iterations = 0;
byte thread = 0;
public RoundFloatToIntAlternate(int cycles, byte threadnumber)
{
iterations = cycles;
thread = threadnumber;
}
public void run()
{
this.setPriority(9);
MathRoundMVCE.longarray[this.thread] = 0;
mainloop();
blankloop();
}
public void blankloop()
{
j = 0;
long timer = 0;
long totaltimer = 0;
do
{
timer = System.nanoTime();
i = 0;
do
{
i++;
}
while (i < 128);
totaltimer += System.nanoTime() - timer;
j++;
}
while (j < iterations);
MathRoundMVCE.longarray[this.thread] -= totaltimer;
}
public void mainloop()
{
j = 0;
long timer = 0;
long totaltimer = 0;
long localsum = 0;
int[] int6 = new int[128];
int[] int7 = new int[128];
int[] int8 = new int[128];
do
{
timer = System.nanoTime();
i = 0;
do
{
int6[i] = Math.round(MathRoundMVCE.float4[i] + MathRoundMVCE.float5[i]);
int7[i] = Math.round(MathRoundMVCE.float4[i] * MathRoundMVCE.float5[i]);
int8[i] = Math.round(MathRoundMVCE.float4[i] / MathRoundMVCE.float5[i]);
i++;
}
while (i < 128);
totaltimer += System.nanoTime() - timer;
for(short z = 0; z < 128; z++)
{
localsum += int6[z] + int7[z] + int8[z];
}
j++;
}
while (j < iterations);
MathRoundMVCE.longarray[this.thread] += totaltimer;
MathRoundMVCE.sumtotal = localsum;
}
}
En pocas palabras, este código se realizó de manera similar en 8u25 que en 8u40. Como puede ver, ahora estoy registrando los resultados de todos los cálculos en matrices, y luego sumando esas matrices fuera de la porción cronometrada del bucle a una variable local que luego se escribe en una variable estática al final del bucle externo.
Bajo 8u25: El tiempo total de ejecución es de 261545 milisegundos.
Bajo 8u40: el tiempo total de ejecución es 266890 milisegundos
Las condiciones de prueba fueron las mismas que antes. Por lo tanto, parece que 8u25 y 8u31 estaban eliminando el código muerto que 8u40 dejó de hacer, lo que provocó que el código se "ralentizara" en 8u40. Eso no explica cada cosa extraña que ha surgido, pero parece ser la mayor parte de ella. Como un bono adicional, las sugerencias y respuestas proporcionadas aquí me han inspirado para mejorar las otras partes de mi proyecto de hobby, por lo que estoy muy agradecido. ¡Gracias a todos por eso!
MVCE basado en OP
- probablemente se puede simplificar aún más
- se cambiaron las declaraciones
int3 =
aint3 +=
para reducir la posibilidad de eliminar el código muerto.int3 =
diferencia de 8u31 a 8u40 es un factor 3x más lento. Usarint3 +=
diferencia es solo un 15% más lenta. - Resultado de impresión para reducir aún más las posibilidades de optimización de eliminación de código muerto
Código
public class MathTime {
static float[][] float1 = new float[8][16];
static float[][] float2 = new float[8][16];
public static void main(String[] args) {
for (int j = 0; j < 8; j++) {
for (int k = 0; k < 16; k++) {
float1[j][k] = (float) (j + k);
float2[j][k] = (float) (j + k);
}
}
new Test().run();
}
private static class Test {
int int3;
public void run() {
for (String test : new String[] { "warmup", "real" }) {
long t0 = System.nanoTime();
for (int count = 0; count < 1e7; count++) {
int i = count % 8;
int3 += Math.round(float1[i][0] + float2[i][0]);
int3 += Math.round(float1[i][1] + float2[i][1]);
int3 += Math.round(float1[i][2] + float2[i][2]);
int3 += Math.round(float1[i][3] + float2[i][3]);
int3 += Math.round(float1[i][4] + float2[i][4]);
int3 += Math.round(float1[i][5] + float2[i][5]);
int3 += Math.round(float1[i][6] + float2[i][6]);
int3 += Math.round(float1[i][7] + float2[i][7]);
int3 += Math.round(float1[i][8] + float2[i][8]);
int3 += Math.round(float1[i][9] + float2[i][9]);
int3 += Math.round(float1[i][10] + float2[i][10]);
int3 += Math.round(float1[i][11] + float2[i][11]);
int3 += Math.round(float1[i][12] + float2[i][12]);
int3 += Math.round(float1[i][13] + float2[i][13]);
int3 += Math.round(float1[i][14] + float2[i][14]);
int3 += Math.round(float1[i][15] + float2[i][15]);
int3 += Math.round(float1[i][0] * float2[i][0]);
int3 += Math.round(float1[i][1] * float2[i][1]);
int3 += Math.round(float1[i][2] * float2[i][2]);
int3 += Math.round(float1[i][3] * float2[i][3]);
int3 += Math.round(float1[i][4] * float2[i][4]);
int3 += Math.round(float1[i][5] * float2[i][5]);
int3 += Math.round(float1[i][6] * float2[i][6]);
int3 += Math.round(float1[i][7] * float2[i][7]);
int3 += Math.round(float1[i][8] * float2[i][8]);
int3 += Math.round(float1[i][9] * float2[i][9]);
int3 += Math.round(float1[i][10] * float2[i][10]);
int3 += Math.round(float1[i][11] * float2[i][11]);
int3 += Math.round(float1[i][12] * float2[i][12]);
int3 += Math.round(float1[i][13] * float2[i][13]);
int3 += Math.round(float1[i][14] * float2[i][14]);
int3 += Math.round(float1[i][15] * float2[i][15]);
int3 += Math.round(float1[i][0] / float2[i][0]);
int3 += Math.round(float1[i][1] / float2[i][1]);
int3 += Math.round(float1[i][2] / float2[i][2]);
int3 += Math.round(float1[i][3] / float2[i][3]);
int3 += Math.round(float1[i][4] / float2[i][4]);
int3 += Math.round(float1[i][5] / float2[i][5]);
int3 += Math.round(float1[i][6] / float2[i][6]);
int3 += Math.round(float1[i][7] / float2[i][7]);
int3 += Math.round(float1[i][8] / float2[i][8]);
int3 += Math.round(float1[i][9] / float2[i][9]);
int3 += Math.round(float1[i][10] / float2[i][10]);
int3 += Math.round(float1[i][11] / float2[i][11]);
int3 += Math.round(float1[i][12] / float2[i][12]);
int3 += Math.round(float1[i][13] / float2[i][13]);
int3 += Math.round(float1[i][14] / float2[i][14]);
int3 += Math.round(float1[i][15] / float2[i][15]);
}
long t1 = System.nanoTime();
System.out.println(int3);
System.out.println(String.format("%s, Math.round(float), %s, %.1f ms", System.getProperty("java.version"), test, (t1 - t0) / 1e6));
}
}
}
}
Resultados
adam@brimstone:~$ ./jdk1.8.0_40/bin/javac MathTime.java;./jdk1.8.0_40/bin/java -cp . MathTime
1.8.0_40, Math.round(float), warmup, 6846.4 ms
1.8.0_40, Math.round(float), real, 6058.6 ms
adam@brimstone:~$ ./jdk1.8.0_31/bin/javac MathTime.java;./jdk1.8.0_31/bin/java -cp . MathTime
1.8.0_31, Math.round(float), warmup, 5717.9 ms
1.8.0_31, Math.round(float), real, 5282.7 ms
adam@brimstone:~$ ./jdk1.8.0_25/bin/javac MathTime.java;./jdk1.8.0_25/bin/java -cp . MathTime
1.8.0_25, Math.round(float), warmup, 5702.4 ms
1.8.0_25, Math.round(float), real, 5262.2 ms
Observaciones
- Para usos triviales de Math.round (float) no puedo encontrar diferencias en el rendimiento en mi plataforma (Linux x86_64). Solo hay una diferencia en el punto de referencia, mis puntos de referencia anteriores ingenuos e incorrectos solo expusieron diferencias en el comportamiento en la optimización, como señalan la respuesta de Ivan y los comentarios de Marco13.
- 8u40 es menos agresivo en la eliminación de código muerto que las versiones anteriores, lo que significa que se ejecuta más código en algunos casos de esquina y, por lo tanto, más lento.
- 8u40 tarda un poco más en calentarse, pero una vez "allí", más rápido.
Análisis de fuente
Sorprendentemente, Math.round (float) es una implementación pura de Java en lugar de nativa, el código para 8u31 y 8u40 es idéntico.
diff jdk1.8.0_31/src/java/lang/Math.java jdk1.8.0_40/src/java/lang/Math.java
-no differences-
public static int round(float a) {
int intBits = Float.floatToRawIntBits(a);
int biasedExp = (intBits & FloatConsts.EXP_BIT_MASK)
>> (FloatConsts.SIGNIFICAND_WIDTH - 1);
int shift = (FloatConsts.SIGNIFICAND_WIDTH - 2
+ FloatConsts.EXP_BIAS) - biasedExp;
if ((shift & -32) == 0) { // shift >= 0 && shift < 32
// a is a finite number such that pow(2,-32) <= ulp(a) < 1
int r = ((intBits & FloatConsts.SIGNIF_BIT_MASK)
| (FloatConsts.SIGNIF_BIT_MASK + 1));
if (intBits < 0) {
r = -r;
}
// In the comments below each Java expression evaluates to the value
// the corresponding mathematical expression:
// (r) evaluates to a / ulp(a)
// (r >> shift) evaluates to floor(a * 2)
// ((r >> shift) + 1) evaluates to floor((a + 1/2) * 2)
// (((r >> shift) + 1) >> 1) evaluates to floor(a + 1/2)
return ((r >> shift) + 1) >> 1;
} else {
// a is either
// - a finite number with abs(a) < exp(2,FloatConsts.SIGNIFICAND_WIDTH-32) < 1/2
// - a finite number with ulp(a) >= 1 and hence a is a mathematical integer
// - an infinity or NaN
return (int) a;
}
}
No es una respuesta definitiva, pero quizás otra pequeña contribución.
Originalmente, repasé toda la cadena como Adam en su respuesta (consulte el historial para más detalles), localizando y comparando códigos de bytes, implementaciones y tiempos de ejecución, aunque, como se señaló en los comentarios, durante mis pruebas (en Win7 / 8) y, con las "mejores prácticas habituales de marca microbiológica", la diferencia de rendimiento no fue tan sorprendente como se sugiere en la pregunta original y en las primeras versiones de las primeras respuestas.
Sin embargo, hubo una diferencia, así que creé otra pequeña prueba:
public class MathRoundPerformance {
static final int size = 16;
static float[] data = new float[size];
public static void main(String[] args) {
for (int i = 0; i < size; i++) {
data[i] = i;
}
for (int n=1000000; n<=100000000; n+=5000000)
{
long t0 = System.nanoTime();
int result = runTest(n);
long t1 = System.nanoTime();
System.out.printf(
"%s, Math.round(float), %s, %s, %.1f ms/n",
System.getProperty("java.version"),
n, result, (t1 - t0) / 1e6);
}
}
public static int runTest(int n) {
int result = 0;
for (int i = 0; i < n; i++) {
int i0 = (i+0) % size;
int i1 = (i+1) % size;
result += Math.round(data[i0] + data[i1]);
result += Math.round(data[i0] * data[i1]);
result += Math.round(data[i0] / data[i1]);
}
return result;
}
}
Los resultados del tiempo (omitiendo algunos detalles) han sido
...
1.8.0_31, Math.round(float), 96000000, -351934592, 504,8 ms
....
1.8.0_40, Math.round(float), 96000000, -351934592, 544,0 ms
Ejecuté los ejemplos con una máquina virtual de desensamblador de hotspot, usando
java -server -XX:+UnlockDiagnosticVMOptions -XX:+TraceClassLoading
-XX:+LogCompilation -XX:+PrintInlining -XX:+PrintAssembly
MathRoundPerformance
Lo importante es que la optimización se termina cuando finaliza el programa (o al menos, parece estar terminado). Esto significa que los resultados de las últimas llamadas al método runTest
se imprimen sin que se runTest
ninguna optimización JIT adicional entre las llamadas.
Traté de descubrir las diferencias mirando el código de máquina generado. Una gran parte del código generado era el mismo para ambas versiones. Pero como señaló Ivan , el número de instrucciones aumentó en 8u40. Comparé el código fuente de las versiones de hotspot u20 y u40. Pensé que podría haber diferencias sutiles en los intrínsecos para floatToRawIntBits , pero estos archivos no cambiaron. Consideré que las comprobaciones de AVX o SSE4.2 que se agregaron recientemente podrían influir en la generación del código de la máquina de manera desafortunada, pero ... mi conocimiento del ensamblador no es tan bueno como me gustaría que fuera, y por lo tanto, No puedo hacer una declaración definitiva aquí. En general, el código de máquina generado parece que fue principalmente reordenado (es decir, principalmente cambiado estructuralmente ), pero la comparación manual de los volcados es un dolor en el ... ojo (las direcciones son todas diferentes, incluso cuando las instrucciones son prácticamente las mismas ).
(Quería volcar los resultados del código de máquina que se genera para el método runTest
aquí, pero hay un límite impar de 30k para una respuesta)
Intentaré analizar más a fondo y comparar los volcados de código de máquina y el código de punto de acceso. Pero al final, será difícil señalar con el dedo "el" cambio que causó la degradación del rendimiento, en términos de código de máquina que se ejecuta más lentamente, así como en términos de cambios en el hotspot que causan el cambio en la máquina. código.