paneles - En Java, ¿puede y ser más rápido que &&?
table swing java (7)
En este código:
if (value >= x && value <= y) {
cuando el
value >= x
value <= y
son tan verdaderos como falsos sin un patrón en particular,
¿sería más rápido usar el operador
&
que usar
&&
?
Específicamente, estoy pensando en cómo
&&
evalúa perezosamente la expresión del lado derecho (es decir, solo si el LHS es verdadero), lo que implica un condicional, mientras que en Java
&
en este contexto garantiza una evaluación estricta de ambas sub-expresiones (booleanas) .
El resultado del valor es el mismo en ambos sentidos.
Pero mientras que un operador
>=
o
<=
utilizará una instrucción de comparación simple, el
&&
debe involucrar una rama, y
esa rama es susceptible al fallo de predicción de rama
, según esta Pregunta muy famosa:
¿Por qué es más rápido procesar una matriz ordenada que una matriz sin clasificar?
Por lo tanto, forzar que la expresión no tenga componentes perezosos seguramente será más determinista y no será vulnerable al fracaso de la predicción. ¿Correcto?
Notas:
-
obviamente, la respuesta a mi pregunta sería
No
si el código se ve así:
if(value >= x && verySlowFunction())
. Me estoy centrando en expresiones "suficientemente simples" de RHS. -
hay una rama condicional allí de todos modos (la declaración
if
). No puedo demostrarme a mí mismo que eso es irrelevante, y que las formulaciones alternativas podrían ser mejores ejemplos, comoboolean b = value >= x && value <= y;
- Todo esto cae en el mundo de las horrendas micro optimizaciones. Sí, lo sé :-) ... interesante sin embargo?
Actualización Solo para explicar por qué estoy interesado: he estado mirando los sistemas sobre los que Martin Thompson ha estado escribiendo en su blog Mechanical Sympathy , después de que vino y habló sobre Aeron. Uno de los mensajes clave es que nuestro hardware tiene todas estas cosas mágicas, y nosotros los desarrolladores de software trágicamente no lo aprovechamos. No se preocupe, no voy a ir s / && / / & / en todo mi código :-) ... pero hay una serie de preguntas en este sitio sobre cómo mejorar la predicción de sucursales eliminando sucursales, y ocurrió para mí, los operadores booleanos condicionales están en el centro de las condiciones de prueba.
Por supuesto, @StephenC hace el punto fantástico de que doblar su código en formas extrañas puede hacer que sea menos fácil para los JIT detectar optimizaciones comunes, si no ahora, en el futuro. Y que la Pregunta muy famosa mencionada anteriormente es especial porque empuja la complejidad de la predicción mucho más allá de la optimización práctica.
Soy bastante consciente de que en la mayoría (o
casi todas
) de las situaciones,
&&
es la mejor manera más clara, más simple, más rápida, ¡aunque estoy muy agradecido con las personas que han publicado respuestas que demuestran esto!
Estoy realmente interesado en ver si realmente hay algún caso en la experiencia de alguien donde la respuesta a "¿Puede
&
sea más rápido?"
podría ser
sí
...
Actualización 2 : (Abordar el consejo de que la pregunta es demasiado amplia. No quiero hacer cambios importantes a esta pregunta porque podría comprometer algunas de las respuestas a continuación, ¡que son de calidad excepcional!) Quizás se llame un ejemplo en la naturaleza para; esto es de la clase Guava LongMath ( muchas gracias a @maaartinus por encontrar esto):
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
¿Ves eso primero?
Y si
lessThanBranchFree(...)
el enlace, el
siguiente
método se llama
lessThanBranchFree(...)
, lo que sugiere que estamos en territorio de evitación de ramas, y la guayaba es muy utilizada: cada ciclo guardado hace que los niveles del mar bajen visiblemente.
Entonces, planteemos la pregunta de esta manera: ¿
es este uso de
&
(donde
&&
sería más normal) una optimización real?
La forma en que esto me fue explicado es que && devolverá falso si la primera verificación de una serie es falsa, mientras que & verifica todos los elementos de una serie independientemente de cuántos sean falsos. ES DECIR
if (x> 0 && x <= 10 && x
Correrá más rápido que
si (x> 0 y x <= 10 y x
Si x es mayor que 10, porque los símbolos de enclavamiento únicos continuarán verificando el resto de las condiciones, mientras que los signos de enclavamiento dobles se romperán después de la primera condición no verdadera.
Lo que buscas es algo como esto:
x <= value & value <= y
value - x >= 0 & y - value >= 0
((value - x) | (y - value)) >= 0 // integer bit-or
Interesante, a uno casi le gustaría mirar el código de bytes. Pero difícil de decir. Desearía que esta fuera una pregunta C.
Tenía curiosidad por la respuesta también, así que escribí la siguiente prueba (simple) para esto:
private static final int max = 80000;
private static final int size = 100000;
private static final int x = 1500;
private static final int y = 15000;
private Random random;
@Before
public void setUp() {
this.random = new Random();
}
@After
public void tearDown() {
random = null;
}
@Test
public void testSingleOperand() {
int counter = 0;
int[] numbers = new int[size];
for (int j = 0; j < size; j++) {
numbers[j] = random.nextInt(max);
}
long start = System.nanoTime(); //start measuring after an array has been filled
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] >= x & numbers[i] <= y) {
counter++;
}
}
long end = System.nanoTime();
System.out.println("Duration of single operand: " + (end - start));
}
@Test
public void testDoubleOperand() {
int counter = 0;
int[] numbers = new int[size];
for (int j = 0; j < size; j++) {
numbers[j] = random.nextInt(max);
}
long start = System.nanoTime(); //start measuring after an array has been filled
for (int i = 0; i < numbers.length; i++) {
if (numbers[i] >= x & numbers[i] <= y) {
counter++;
}
}
long end = System.nanoTime();
System.out.println("Duration of double operand: " + (end - start));
}
El resultado final es que la comparación con && siempre gana en términos de velocidad, siendo aproximadamente 1.5 / 2 milisegundos más rápido que &.
EDITAR: Como señaló @SvetlinZarev, también estaba midiendo el tiempo que le tomó a Random obtener un número entero. Lo cambió para usar una matriz precargada de números aleatorios, lo que hizo que la duración de la prueba de un solo operando fluctuara enormemente; Las diferencias entre varias carreras fueron de hasta 6-7ms.
El uso de
&
o
&
todavía requiere que se evalúe una condición, por lo que es poco probable que ahorre tiempo de procesamiento; incluso podría agregarse considerando que está evaluando ambas expresiones cuando solo necesita evaluar una.
Usar
&
over
&&
para ahorrar un nanosegundo si eso en algunas situaciones muy raras no tiene sentido, ya ha perdido más tiempo contemplando la diferencia de lo que habría ahorrado usando
&
over
&&
.
Editar
Me dio curiosidad y decidí hacer algunos puntos de referencia.
Hice esta clase:
public class Main {
static int x = 22, y = 48;
public static void main(String[] args) {
runWithOneAnd(30);
runWithTwoAnds(30);
}
static void runWithOneAnd(int value){
if(value >= x & value <= y){
}
}
static void runWithTwoAnds(int value){
if(value >= x && value <= y){
}
}
}
y ejecuté algunas pruebas de perfil con NetBeans.
No utilicé ninguna declaración impresa para ahorrar tiempo de procesamiento, solo sé que ambas se evalúan como
true
.
Primer examen:
Segunda prueba:
Tercera prueba:
Como puede ver en las pruebas de creación de perfiles, usar solo una
&
realidad tarda 2-3 veces más en ejecutarse en comparación con el uso de dos
&&
.
Esto parece sorprendente, ya que esperaba un mejor rendimiento de solo uno.
No estoy 100% seguro de por qué. En ambos casos, ambas expresiones tienen que ser evaluadas porque ambas son verdaderas. Sospecho que la JVM hace una optimización especial detrás de escena para acelerarlo.
Moraleja de la historia: la convención es buena y la optimización prematura es mala.
Editar 2
Rehice el código de referencia teniendo en cuenta los comentarios de @ SvetlinZarev y algunas otras mejoras. Aquí está el código de referencia modificado:
public class Main {
static int x = 22, y = 48;
public static void main(String[] args) {
oneAndBothTrue();
oneAndOneTrue();
oneAndBothFalse();
twoAndsBothTrue();
twoAndsOneTrue();
twoAndsBothFalse();
System.out.println(b);
}
static void oneAndBothTrue() {
int value = 30;
for (int i = 0; i < 2000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}
static void oneAndOneTrue() {
int value = 60;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}
static void oneAndBothFalse() {
int value = 100;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}
static void twoAndsBothTrue() {
int value = 30;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}
static void twoAndsOneTrue() {
int value = 60;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}
static void twoAndsBothFalse() {
int value = 100;
for (int i = 0; i < 4000; i++) {
if (value >= x & value <= y) {
doSomething();
}
}
}
//I wanted to avoid print statements here as they can
//affect the benchmark results.
static StringBuilder b = new StringBuilder();
static int times = 0;
static void doSomething(){
times++;
b.append("I have run ").append(times).append(" times /n");
}
}
Y aquí están las pruebas de rendimiento:
Prueba 1:
Prueba 2:
Prueba 3:
Esto tiene en cuenta diferentes valores y diferentes condiciones también.
Usar uno
&
requiere más tiempo para ejecutarse cuando ambas condiciones son verdaderas, aproximadamente 60% o 2 milisegundos más de tiempo.
Cuando una o ambas condiciones son falsas, una se
&
ejecuta más rápido, pero solo se ejecuta entre 0,30 y 0,50 milisegundos más rápido.
Por
&
lo
tanto
, funcionará más rápido que
&&
en la mayoría de las circunstancias, pero la diferencia de rendimiento sigue siendo insignificante.
Ok, entonces quieres saber cómo se comporta en el nivel inferior ... ¡Veamos el código de bytes entonces!
EDITAR: se agregó el código de ensamblaje generado para AMD64, al final.
Echa un vistazo a algunas notas interesantes.
EDIT 2 (re: "Actualización 2" de OP): también se agregó código asm para
el método
isPowerOfTwo
de Guava
.
Fuente de Java
Escribí estos dos métodos rápidos:
public boolean AndSC(int x, int value, int y) {
return value >= x && value <= y;
}
public boolean AndNonSC(int x, int value, int y) {
return value >= x & value <= y;
}
Como puede ver, son exactamente iguales, salvo por el tipo de operador AND.
Código de bytes de Java
Y este es el bytecode generado:
public AndSC(III)Z
L0
LINENUMBER 8 L0
ILOAD 2
ILOAD 1
IF_ICMPLT L1
ILOAD 2
ILOAD 3
IF_ICMPGT L1
L2
LINENUMBER 9 L2
ICONST_1
IRETURN
L1
LINENUMBER 11 L1
FRAME SAME
ICONST_0
IRETURN
L3
LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L3 0
LOCALVARIABLE x I L0 L3 1
LOCALVARIABLE value I L0 L3 2
LOCALVARIABLE y I L0 L3 3
MAXSTACK = 2
MAXLOCALS = 4
// access flags 0x1
public AndNonSC(III)Z
L0
LINENUMBER 15 L0
ILOAD 2
ILOAD 1
IF_ICMPLT L1
ICONST_1
GOTO L2
L1
FRAME SAME
ICONST_0
L2
FRAME SAME1 I
ILOAD 2
ILOAD 3
IF_ICMPGT L3
ICONST_1
GOTO L4
L3
FRAME SAME1 I
ICONST_0
L4
FRAME FULL [test/lsoto/AndTest I I I] [I I]
IAND
IFEQ L5
L6
LINENUMBER 16 L6
ICONST_1
IRETURN
L5
LINENUMBER 18 L5
FRAME SAME
ICONST_0
IRETURN
L7
LOCALVARIABLE this Ltest/lsoto/AndTest; L0 L7 0
LOCALVARIABLE x I L0 L7 1
LOCALVARIABLE value I L0 L7 2
LOCALVARIABLE y I L0 L7 3
MAXSTACK = 3
MAXLOCALS = 4
El
AndSC
(
&&
) genera
dos
saltos condicionales, como se esperaba:
-
Carga el
value
x
en la pila, y salta a L1 si elvalue
es menor. De lo contrario, sigue corriendo las siguientes líneas. -
Carga
value
y
en la pila, y también salta a L1, si elvalue
es mayor. De lo contrario, sigue corriendo las siguientes líneas. -
Lo que resulta ser un
return true
en caso de que ninguno de los dos saltos se haya realizado. -
Y luego tenemos las líneas marcadas como L1 que son un
return false
.
¡El
AndNonSC
(
&
), sin embargo, genera
tres
saltos condicionales!
-
Carga el
value
x
en la pila y salta a L1 si elvalue
es menor. Debido a que ahora necesita guardar el resultado para compararlo con la otra parte del AND, por lo que debe ejecutar "guardartrue
" o "guardarfalse
", no puede hacer ambas cosas con la misma instrucción. -
Carga
value
ey
en la pila y salta a L1 si elvalue
es mayor. Una vez más, necesita guardartrue
ofalse
y eso son dos líneas diferentes dependiendo del resultado de la comparación. - Ahora que ambas comparaciones están hechas, el código realmente ejecuta la operación AND, y si ambas son verdaderas, salta (por tercera vez) para devolver verdadero; o bien continúa la ejecución en la siguiente línea para devolver falso.
(Preliminar) Conclusión
Aunque no tengo mucha experiencia con el código de bytes de Java y es posible que haya pasado por alto algo, me parece que
&
realidad funcionará
peor
que
&&
en todos los casos: genera más instrucciones para ejecutar, incluidos más saltos condicionales para predecir y posiblemente fallar en
Una reescritura del código para reemplazar las comparaciones con operaciones aritméticas, como alguien más propuso, podría ser una forma de hacer una mejor opción, pero a costa de hacer que el código sea mucho menos claro.
En mi humilde opinión, no vale la pena el 99% de los escenarios (puede valer la pena para los bucles de 1% que necesitan ser extremadamente optimizados).
EDITAR: montaje AMD64
Como se señaló en los comentarios, el mismo código de bytes de Java puede conducir a un código de máquina diferente en diferentes sistemas, por lo que si bien el código de bytes de Java podría darnos una pista sobre qué versión AND funciona mejor, obtener el ASM real generado por el compilador es la única forma para descubrirlo realmente
Imprimí las instrucciones AMD64 ASM para ambos métodos;
a continuación están las líneas relevantes (puntos de entrada eliminados, etc.).
NOTA: todos los métodos compilados con java 1.8.0_91 a menos que se indique lo contrario.
Método
AndSC
con opciones predeterminadas
# {method} {0x0000000016da0810} ''AndSC'' ''(III)Z'' in ''AndTest''
...
0x0000000002923e3e: cmp %r8d,%r9d
0x0000000002923e41: movabs $0x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} ''AndSC'' ''(III)Z'' in ''AndTest'')}
0x0000000002923e4b: movabs $0x108,%rsi
0x0000000002923e55: jl 0x0000000002923e65
0x0000000002923e5b: movabs $0x118,%rsi
0x0000000002923e65: mov (%rax,%rsi,1),%rbx
0x0000000002923e69: lea 0x1(%rbx),%rbx
0x0000000002923e6d: mov %rbx,(%rax,%rsi,1)
0x0000000002923e71: jl 0x0000000002923eb0 ;*if_icmplt
; - AndTest::AndSC@2 (line 22)
0x0000000002923e77: cmp %edi,%r9d
0x0000000002923e7a: movabs $0x16da0a08,%rax ; {metadata(method data for {method} {0x0000000016da0810} ''AndSC'' ''(III)Z'' in ''AndTest'')}
0x0000000002923e84: movabs $0x128,%rsi
0x0000000002923e8e: jg 0x0000000002923e9e
0x0000000002923e94: movabs $0x138,%rsi
0x0000000002923e9e: mov (%rax,%rsi,1),%rdi
0x0000000002923ea2: lea 0x1(%rdi),%rdi
0x0000000002923ea6: mov %rdi,(%rax,%rsi,1)
0x0000000002923eaa: jle 0x0000000002923ec1 ;*if_icmpgt
; - AndTest::AndSC@7 (line 22)
0x0000000002923eb0: mov $0x0,%eax
0x0000000002923eb5: add $0x30,%rsp
0x0000000002923eb9: pop %rbp
0x0000000002923eba: test %eax,-0x1c73dc0(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923ec0: retq ;*ireturn
; - AndTest::AndSC@13 (line 25)
0x0000000002923ec1: mov $0x1,%eax
0x0000000002923ec6: add $0x30,%rsp
0x0000000002923eca: pop %rbp
0x0000000002923ecb: test %eax,-0x1c73dd1(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923ed1: retq
Método
AndSC
con
-XX:PrintAssemblyOptions=intel
opción
-XX:PrintAssemblyOptions=intel
# {method} {0x00000000170a0810} ''AndSC'' ''(III)Z'' in ''AndTest''
...
0x0000000002c26e2c: cmp r9d,r8d
0x0000000002c26e2f: jl 0x0000000002c26e36 ;*if_icmplt
0x0000000002c26e31: cmp r9d,edi
0x0000000002c26e34: jle 0x0000000002c26e44 ;*iconst_0
0x0000000002c26e36: xor eax,eax ;*synchronization entry
0x0000000002c26e38: add rsp,0x10
0x0000000002c26e3c: pop rbp
0x0000000002c26e3d: test DWORD PTR [rip+0xffffffffffce91bd],eax # 0x0000000002910000
0x0000000002c26e43: ret
0x0000000002c26e44: mov eax,0x1
0x0000000002c26e49: jmp 0x0000000002c26e38
Método
AndNonSC
con opciones predeterminadas
# {method} {0x0000000016da0908} ''AndNonSC'' ''(III)Z'' in ''AndTest''
...
0x0000000002923a78: cmp %r8d,%r9d
0x0000000002923a7b: mov $0x0,%eax
0x0000000002923a80: jl 0x0000000002923a8b
0x0000000002923a86: mov $0x1,%eax
0x0000000002923a8b: cmp %edi,%r9d
0x0000000002923a8e: mov $0x0,%esi
0x0000000002923a93: jg 0x0000000002923a9e
0x0000000002923a99: mov $0x1,%esi
0x0000000002923a9e: and %rsi,%rax
0x0000000002923aa1: cmp $0x0,%eax
0x0000000002923aa4: je 0x0000000002923abb ;*ifeq
; - AndTest::AndNonSC@21 (line 29)
0x0000000002923aaa: mov $0x1,%eax
0x0000000002923aaf: add $0x30,%rsp
0x0000000002923ab3: pop %rbp
0x0000000002923ab4: test %eax,-0x1c739ba(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923aba: retq ;*ireturn
; - AndTest::AndNonSC@25 (line 30)
0x0000000002923abb: mov $0x0,%eax
0x0000000002923ac0: add $0x30,%rsp
0x0000000002923ac4: pop %rbp
0x0000000002923ac5: test %eax,-0x1c739cb(%rip) # 0x0000000000cb0100
; {poll_return}
0x0000000002923acb: retq
Método
AndNonSC
con
-XX:PrintAssemblyOptions=intel
opción
-XX:PrintAssemblyOptions=intel
# {method} {0x00000000170a0908} ''AndNonSC'' ''(III)Z'' in ''AndTest''
...
0x0000000002c270b5: cmp r9d,r8d
0x0000000002c270b8: jl 0x0000000002c270df ;*if_icmplt
0x0000000002c270ba: mov r8d,0x1 ;*iload_2
0x0000000002c270c0: cmp r9d,edi
0x0000000002c270c3: cmovg r11d,r10d
0x0000000002c270c7: and r8d,r11d
0x0000000002c270ca: test r8d,r8d
0x0000000002c270cd: setne al
0x0000000002c270d0: movzx eax,al
0x0000000002c270d3: add rsp,0x10
0x0000000002c270d7: pop rbp
0x0000000002c270d8: test DWORD PTR [rip+0xffffffffffce8f22],eax # 0x0000000002910000
0x0000000002c270de: ret
0x0000000002c270df: xor r8d,r8d
0x0000000002c270e2: jmp 0x0000000002c270c0
- En primer lugar, el código ASM generado difiere dependiendo de si elegimos la sintaxis AT&T predeterminada o la sintaxis Intel.
-
Con la sintaxis de AT&T:
-
El código ASM es en realidad
más largo
para el método
AndSC
, con cada bytecodeIF_ICMP*
traducido a dos instrucciones de salto de ensamblaje, para un total de 4 saltos condicionales. -
Mientras tanto, para el método
AndNonSC
, el compilador genera un código más directo, donde cada bytecodeIF_ICMP*
se traduce a una sola instrucción de salto de ensamblaje, manteniendo el recuento original de 3 saltos condicionales.
-
El código ASM es en realidad
más largo
para el método
-
Con la sintaxis de Intel:
-
El código ASM para
AndSC
es más corto, con solo 2 saltos condicionales (sin contar eljmp
no condicional al final). En realidad son solo dos CMP, dos JL / E y un XOR / MOV, dependiendo del resultado. -
¡El código ASM para
AndNonSC
ahora es más largo que el deAndSC
! Sin embargo , tiene solo 1 salto condicional (para la primera comparación), utilizando los registros para comparar directamente el primer resultado con el segundo, sin más saltos.
-
El código ASM para
Conclusión después del análisis de código ASM
-
A nivel de lenguaje de máquina AMD64, el operador
&
parece generar código ASM con menos saltos condicionales, lo que podría ser mejor para altas tasas de falla de predicción (value
aleatoriosvalue
por ejemplo). -
Por otro lado, el operador
&&
parece generar código ASM con menos instrucciones (con la-XX:PrintAssemblyOptions=intel
todos modos), lo que podría ser mejor para bucles realmente largos con entradas amigables para la predicción, donde la menor cantidad de ciclos de CPU para cada comparación puede hacer la diferencia a largo plazo.
Como dije en algunos de los comentarios, esto va a variar mucho entre sistemas, por lo que si hablamos de optimización de predicción de sucursales, la única respuesta real sería: depende de su implementación de JVM, su compilador, su CPU y sus datos de entrada
Anexo: Método
isPowerOfTwo
de Guava
Aquí, los desarrolladores de Guava han ideado una forma ordenada de calcular si un número dado es una potencia de 2:
public static boolean isPowerOfTwo(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
Citando OP:
¿Es este uso de
&
(donde&&
sería más normal) una optimización real?
Para saber si es así, agregué dos métodos similares a mi clase de prueba:
public boolean isPowerOfTwoAND(long x) {
return x > 0 & (x & (x - 1)) == 0;
}
public boolean isPowerOfTwoANDAND(long x) {
return x > 0 && (x & (x - 1)) == 0;
}
Código ASM de Intel para la versión de Guava
# {method} {0x0000000017580af0} ''isPowerOfTwoAND'' ''(J)Z'' in ''AndTest''
# this: rdx:rdx = ''AndTest''
# parm0: r8:r8 = long
...
0x0000000003103bbe: movabs rax,0x0
0x0000000003103bc8: cmp rax,r8
0x0000000003103bcb: movabs rax,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} ''isPowerOfTwoAND'' ''(J)Z'' in ''AndTest'')}
0x0000000003103bd5: movabs rsi,0x108
0x0000000003103bdf: jge 0x0000000003103bef
0x0000000003103be5: movabs rsi,0x118
0x0000000003103bef: mov rdi,QWORD PTR [rax+rsi*1]
0x0000000003103bf3: lea rdi,[rdi+0x1]
0x0000000003103bf7: mov QWORD PTR [rax+rsi*1],rdi
0x0000000003103bfb: jge 0x0000000003103c1b ;*lcmp
0x0000000003103c01: movabs rax,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} ''isPowerOfTwoAND'' ''(J)Z'' in ''AndTest'')}
0x0000000003103c0b: inc DWORD PTR [rax+0x128]
0x0000000003103c11: mov eax,0x1
0x0000000003103c16: jmp 0x0000000003103c20 ;*goto
0x0000000003103c1b: mov eax,0x0 ;*lload_1
0x0000000003103c20: mov rsi,r8
0x0000000003103c23: movabs r10,0x1
0x0000000003103c2d: sub rsi,r10
0x0000000003103c30: and rsi,r8
0x0000000003103c33: movabs rdi,0x0
0x0000000003103c3d: cmp rsi,rdi
0x0000000003103c40: movabs rsi,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} ''isPowerOfTwoAND'' ''(J)Z'' in ''AndTest'')}
0x0000000003103c4a: movabs rdi,0x140
0x0000000003103c54: jne 0x0000000003103c64
0x0000000003103c5a: movabs rdi,0x150
0x0000000003103c64: mov rbx,QWORD PTR [rsi+rdi*1]
0x0000000003103c68: lea rbx,[rbx+0x1]
0x0000000003103c6c: mov QWORD PTR [rsi+rdi*1],rbx
0x0000000003103c70: jne 0x0000000003103c90 ;*lcmp
0x0000000003103c76: movabs rsi,0x175811f0 ; {metadata(method data for {method} {0x0000000017580af0} ''isPowerOfTwoAND'' ''(J)Z'' in ''AndTest'')}
0x0000000003103c80: inc DWORD PTR [rsi+0x160]
0x0000000003103c86: mov esi,0x1
0x0000000003103c8b: jmp 0x0000000003103c95 ;*goto
0x0000000003103c90: mov esi,0x0 ;*iand
0x0000000003103c95: and rsi,rax
0x0000000003103c98: and esi,0x1
0x0000000003103c9b: mov rax,rsi
0x0000000003103c9e: add rsp,0x50
0x0000000003103ca2: pop rbp
0x0000000003103ca3: test DWORD PTR [rip+0xfffffffffe44c457],eax # 0x0000000001550100
0x0000000003103ca9: ret
Código ASM de Intel para la versión
&&
# {method} {0x0000000017580bd0} ''isPowerOfTwoANDAND'' ''(J)Z'' in ''AndTest''
# this: rdx:rdx = ''AndTest''
# parm0: r8:r8 = long
...
0x0000000003103438: movabs rax,0x0
0x0000000003103442: cmp rax,r8
0x0000000003103445: jge 0x0000000003103471 ;*lcmp
0x000000000310344b: mov rax,r8
0x000000000310344e: movabs r10,0x1
0x0000000003103458: sub rax,r10
0x000000000310345b: and rax,r8
0x000000000310345e: movabs rsi,0x0
0x0000000003103468: cmp rax,rsi
0x000000000310346b: je 0x000000000310347b ;*lcmp
0x0000000003103471: mov eax,0x0
0x0000000003103476: jmp 0x0000000003103480 ;*ireturn
0x000000000310347b: mov eax,0x1 ;*goto
0x0000000003103480: and eax,0x1
0x0000000003103483: add rsp,0x40
0x0000000003103487: pop rbp
0x0000000003103488: test DWORD PTR [rip+0xfffffffffe44cc72],eax # 0x0000000001550100
0x000000000310348e: ret
En este ejemplo específico, el compilador JIT genera
mucho
menos código de ensamblaje para la versión
&&
que para la versión
&
de Guava (y, después de los resultados de ayer, honestamente me sorprendió esto).
En comparación con Guava''s, la versión
&&
traduce en un 25% menos de bytecode para que JIT compile, un 50% menos de instrucciones de ensamblaje y solo dos saltos condicionales (la versión
&
tiene cuatro de ellos).
Entonces, todo apunta a que el método
&
la guayaba es menos eficiente que la versión
&&
más "natural".
... ¿O es eso?
Como se señaló anteriormente, estoy ejecutando los ejemplos anteriores con Java 8:
C:/....>java -version
java version "1.8.0_91"
Java(TM) SE Runtime Environment (build 1.8.0_91-b14)
Java HotSpot(TM) 64-Bit Server VM (build 25.91-b14, mixed mode)
Pero, ¿y si me cambio a Java 7 ?
C:/....>c:/jdk1.7.0_79/bin/java -version
java version "1.7.0_79"
Java(TM) SE Runtime Environment (build 1.7.0_79-b15)
Java HotSpot(TM) 64-Bit Server VM (build 24.79-b02, mixed mode)
C:/....>c:/jdk1.7.0_79/bin/java -XX:+UnlockDiagnosticVMOptions -XX:CompileCommand=print,*AndTest.isPowerOfTwoAND -XX:PrintAssemblyOptions=intel AndTestMain
.....
0x0000000002512bac: xor r10d,r10d
0x0000000002512baf: mov r11d,0x1
0x0000000002512bb5: test r8,r8
0x0000000002512bb8: jle 0x0000000002512bde ;*ifle
0x0000000002512bba: mov eax,0x1 ;*lload_1
0x0000000002512bbf: mov r9,r8
0x0000000002512bc2: dec r9
0x0000000002512bc5: and r9,r8
0x0000000002512bc8: test r9,r9
0x0000000002512bcb: cmovne r11d,r10d
0x0000000002512bcf: and eax,r11d ;*iand
0x0000000002512bd2: add rsp,0x10
0x0000000002512bd6: pop rbp
0x0000000002512bd7: test DWORD PTR [rip+0xffffffffffc0d423],eax # 0x0000000002120000
0x0000000002512bdd: ret
0x0000000002512bde: xor eax,eax
0x0000000002512be0: jmp 0x0000000002512bbf
.....
¡Sorpresa!
El código de ensamblaje generado para el método
&
por el compilador JIT en Java 7, ahora solo tiene
un
salto condicional, ¡y es mucho más corto!
Mientras que el método
&&
(tendrás que confiar en mí en este caso, ¡no quiero saturar el final!) Sigue siendo el mismo, con sus dos saltos condicionales y un par de instrucciones menos, como máximo.
¡Parece que los ingenieros de Guava sabían lo que estaban haciendo, después de todo!
(si intentaban optimizar el tiempo de ejecución de Java 7, eso es ;-)
Volviendo a la última pregunta de OP:
¿Es este uso de
&
(donde&&
sería más normal) una optimización real?
Y en mi humilde opinión, la respuesta es la misma , incluso para este (¡muy!) Escenario específico: depende de su implementación de JVM, su compilador, su CPU y sus datos de entrada .
Para ese tipo de preguntas, debe ejecutar un microbenchmark. JMH para esta prueba.
Los puntos de referencia se implementan como
// boolean logical AND
bh.consume(value >= x & y <= value);
y
// conditional AND
bh.consume(value >= x && y <= value);
y
// bitwise OR, as suggested by Joop Eggen
bh.consume(((value - x) | (y - value)) >= 0)
Con valores de
value, x and y
según el nombre de referencia.
El resultado (cinco calentamientos y diez iteraciones de medición) para la evaluación comparativa de rendimiento es:
Benchmark Mode Cnt Score Error Units
Benchmark.isBooleanANDBelowRange thrpt 10 386.086 ▒ 17.383 ops/us
Benchmark.isBooleanANDInRange thrpt 10 387.240 ▒ 7.657 ops/us
Benchmark.isBooleanANDOverRange thrpt 10 381.847 ▒ 15.295 ops/us
Benchmark.isBitwiseORBelowRange thrpt 10 384.877 ▒ 11.766 ops/us
Benchmark.isBitwiseORInRange thrpt 10 380.743 ▒ 15.042 ops/us
Benchmark.isBitwiseOROverRange thrpt 10 383.524 ▒ 16.911 ops/us
Benchmark.isConditionalANDBelowRange thrpt 10 385.190 ▒ 19.600 ops/us
Benchmark.isConditionalANDInRange thrpt 10 384.094 ▒ 15.417 ops/us
Benchmark.isConditionalANDOverRange thrpt 10 380.913 ▒ 5.537 ops/us
El resultado no es tan diferente para la evaluación en sí. Mientras no se vea ningún impacto en el rendimiento de ese fragmento de código, no intentaré optimizarlo. Dependiendo del lugar en el código, el compilador de puntos de acceso podría decidir hacer alguna optimización. Lo que probablemente no esté cubierto por los puntos de referencia anteriores.
algunas referencias:
AND lógico booleano
: el valor del resultado es
true
si ambos valores de operando son
true
;
de lo contrario, el resultado es
false
condicional AND
- es como
&
, pero evalúa su operando de la derecha solo si el valor de su operando de la izquierda es
true
OR bit a bit
: el valor del resultado es el OR bit a bit inclusivo de los valores de operando
Voy a llegar a esto desde un ángulo diferente.
Considere estos dos fragmentos de código,
if (value >= x && value <= y) {
y
if (value >= x & value <= y) {
Si asumimos que el
value
,
x
,
y
tiene un tipo primitivo, esas dos declaraciones (parciales) darán el mismo resultado para todos los posibles valores de entrada.
(Si están involucrados tipos de envoltura, entonces no son exactamente equivalentes debido a una prueba
null
implícita para
y
que podría fallar en la versión
&
y no en la versión
&&
).
Si el compilador JIT está haciendo un buen trabajo, su optimizador podrá deducir que esas dos declaraciones hacen lo mismo:
-
Si uno es previsiblemente más rápido que el otro, entonces debería poder usar la versión más rápida ... en el código compilado JIT .
-
De lo contrario, no importa qué versión se use en el nivel del código fuente.
-
Dado que el compilador JIT recopila estadísticas de ruta antes de compilar, potencialmente puede tener más información sobre las características de ejecución que el programador (!).
-
Si el compilador JIT de la generación actual (en cualquier plataforma dada) no se optimiza lo suficientemente bien como para manejar esto, la próxima generación podría hacerlo ... dependiendo de si la evidencia empírica indica que este es un patrón que vale la pena optimizar.
-
De hecho, si escribe su código Java de una manera que se optimice para esto, existe la posibilidad de que al elegir la versión más "oscura" del código, pueda inhibir la capacidad actual o futura del compilador JIT para optimizar.
En resumen, no creo que deba hacer este tipo de microoptimización a nivel de código fuente. Y si acepta este argumento 1 y lo sigue hasta su conclusión lógica, la pregunta de qué versión es más rápida es ... discutible 2 .
1 - No afirmo que esto esté cerca de ser una prueba.
2 - A menos que seas una de la pequeña comunidad de personas que realmente escriben compiladores Java JIT ...
La "Pregunta muy famosa" es interesante en dos aspectos:
-
Por un lado, ese es un ejemplo en el que el tipo de optimización necesaria para marcar la diferencia va más allá de la capacidad de un compilador JIT.
-
Por otro lado, no necesariamente sería lo correcto clasificar la matriz ... solo porque una matriz ordenada se puede procesar más rápido. El costo de ordenar la matriz podría ser (mucho) mayor que el ahorro.