Prueba de rendimiento del código de Linux con ejemplos

Cuando comencé a aprender Java, una de las primeras tareas que intenté resolver fue determinar los números pares / impares. Conocía varias formas de hacer esto, pero decidí buscar la forma "correcta" en Internet. La información en todos los enlaces encontrados me habló de la única solución correcta de la forma x% 2, para obtener el resto de la división. Si el resto es 0, el número es par; si el resto es 1, es impar.

Desde la época de ZX Spectrum, recordé otra forma y está asociada con la representación de números en el sistema binario. Cualquier número en el sistema decimal se puede escribir como la suma de las potencias de dos. Por ejemplo, para un byte, y esto es 8 bits, cualquier número en el sistema decimal se puede representar como la suma de los números 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1.

Esto es solo una secuencia de poderes de dos. Al traducir un número al sistema binario, si tenemos que tener en cuenta el número, en la representación binaria será uno, si no es necesario, será 0.

Por ejemplo:

10 = 1010 (8 + 0 + 2 + 0)
13 = 1101 (8 + 4 + 0 + 1)
200 = 11001000 (128 + 64 + 0 + 0 + 8 + 0 + 0 + 0)

Puede prestar atención de inmediato al hecho de que un número impar solo puede dar una potencia cero de dos con un valor de 1, todas las demás potencias de dos serán pares por definición. Esto significa automáticamente que, desde el punto de vista del sistema de números binarios, si queremos verificar la paridad de cualquier número, no necesitamos verificar el número entero, sin importar cuán grande sea. Necesitamos verificar solo el primer bit (el más a la derecha). Si es 0, entonces el número es par, ya que todos los demás bits dan un número par, y viceversa, si el bit más a la derecha es uno, entonces se garantiza que el número es impar, porque todos los demás bits solo dan un valor par.
Para verificar solo el bit correcto en un número, puede usar varios métodos. Uno de ellos es binario AND.

Y


Binario AND (AND) funciona según la siguiente regla. Si aplica a cualquier número, llamémoslo original, lógico Y con el número 0, entonces el resultado de tal operación es siempre 0. De esta manera puede poner a cero los bits que no necesita. Si solicita el original 1, obtendrá el original.

En un sistema binario, es fácil escribir esto:

0 Y 0 = 0 (cero el original)
1 Y 0 = 0 (cero el original)
0 Y 1 = 0 (no cambie el original)
1 Y 1 = 1 (no cambie el original)

De aquí algunos simples reglas.

Si aplicamos la operación AND de todas las unidades a todos los números (todos los bits están activados), obtenemos el mismo número inicial.

Si aplicamos AND de todos los ceros a cualquier número (todos los bits están apagados), obtenemos 0.

Por ejemplo:

Si aplicamos AND 0 al byte 13, entonces obtenemos 0. En decimal parece 13 AND 0 = 0

Si aplicamos AND 0 al byte 200, obtenemos 0, o escribimos 200 AND 0 = 0 brevemente.
Lo mismo es lo contrario, aplicamos a 13 todos los bits incluidos, para un byte serán ocho unidades, y obtenemos el original. En el sistema binario 00001101 Y 11111111 = 00001101 o en el sistema decimal 13 Y 255 = 13

Para 200 habrá 11001000 Y 11111111 = 11001000, respectivamente, o en el sistema decimal 200 Y 255 = 200

Verificación binaria


Para verificar el número de paridad, solo necesitamos verificar el bit más a la derecha. Si es 0, entonces el número es par; si es 1, entonces no es par. Sabiendo que con AND podemos dejar algunos bits originales, y algunos podemos restablecer, simplemente podemos restablecer todos los bits, excepto el más a la derecha. Por ejemplo:

13 en el sistema binario es 1101. Apliquemos AND 0001 (restablecemos todos los bits, el último sigue siendo el original). En 1101, cambiamos todos los bits a 0 excepto el último y obtenemos 0001. Obtuvimos solo el último bit de nuestro número original. En el sistema decimal, se verá como 13 Y 1 = 1.

Lo mismo con el número 200, en binario 11001000. Le aplicamos AND 00000001, de acuerdo con el mismo esquema, ponemos a cero todos los bits, dejamos el último como está, obtenemos 00000000, mientras que los 7 ceros izquierdos se restablecieron con el comando AND, y el último 0 lo obtuvimos del número original En el sistema decimal, parece 200 AND 1 = 0

Por lo tanto, aplicando el comando AND 1 a cualquier número, obtenemos 0 o 1. Y si el resultado es 0, entonces el número es par. Cuando 1, el número es impar.

En Java, el AND binario se escribe como &. En consecuencia, 200 y 1 = 0 (par) y 13 y 1 = 1 (impar).

Esto implica al menos dos métodos para determinar números pares.

X% 2 - a través del resto de la división por dos
X y 1 - a través de binario AND

El procesador procesa operaciones binarias como OR, AND, XOR en un tiempo mínimo. Pero la operación de división no es una tarea trivial, y para ejecutarla, el procesador necesita procesar muchas instrucciones, esencialmente ejecutar todo el programa. Sin embargo, hay operaciones binarias de desplazamiento hacia la izquierda y hacia la derecha que permiten, por ejemplo, dividir rápidamente un número entre 2. La pregunta es si los compiladores usan esta optimización y si hay una diferencia entre estas dos comparaciones, que de hecho hacen lo mismo.

Codificación


Escribiremos un programa que procesará 9,000,000,000 números en un ciclo en orden, y determinaremos su pertenencia a par / impar determinando el resto de la división.

public class OddEvenViaMod {
        public static void main (String[] args) {
                long i=0;
                long odds=0;
                long evens=0;
                do {
                if ((i % 2) == 0) {
                        evens++;
                        }
                else {
                        odds++;
                        }
                i++;
                } while (i<9000000000L);
                System.out.println("Odd " + odds);
                System.out.println("Even " + evens);
        }
}

Y escribiremos exactamente lo mismo, pero literalmente cambiaremos dos caracteres, verificando lo mismo a través del binario AND.

public class OddEvenViaAnd {
        public static void main (String[] args) {
                long i=0;
                long odds=0;
                long evens=0;
                do {
                if ((i & 1) == 0) {
                        evens++;
                        }
                else {
                        odds++;
                        }
                i++;
                } while (i<9000000000L);
                System.out.println("Odd " + odds);
                System.out.println("Even " + evens);

Ahora tenemos que comparar de alguna manera estos dos programas.

Recursos en Linux. UPC


Se ha invertido una gran cantidad de horas en la creación de cualquier sistema operativo, en particular en la distribución justa de recursos entre programas. Por un lado, esto es bueno, ya que al ejecutar dos programas, puede estar seguro de que funcionarán en paralelo, pero por otro lado, cuando necesita verificar el rendimiento de un programa, es extremadamente necesario limitar o al menos reducir la influencia externa en el programa de otros. programas y sistema operativo.

Lo primero que debes descubrir es el procesador. El sistema operativo Linux para cada proceso almacena una máscara de bits, que indica qué núcleos puede utilizar la aplicación y cuáles no. Puede ver y cambiar esta máscara con el comando del conjunto de tareas.

Por ejemplo, veamos la cantidad de núcleos en mi procesador:

[user@localhost]# grep -c processor /proc/cpuinfo
4

Mi computadora tiene un procesador con 4 núcleos. Esto es bueno, porque voy a asignar uno de ellos a mis necesidades.

Veamos si todos están actualmente en uso con el comando superior:

[user@localhost]# top

Presione "1" para ver la información de cada núcleo por separado:

top - 13:44:11 up 1 day, 23:26,  7 users,  load average: 1.48, 2.21, 2.02
Tasks: 321 total,   1 running, 320 sleeping,   0 stopped,   0 zombie
%Cpu0  :  7.7 us,  6.8 sy,  0.0 ni, 85.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  9.2 us,  4.2 sy,  0.0 ni, 86.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  7.6 us,  3.4 sy,  0.0 ni, 89.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  8.4 us,  4.2 sy,  0.0 ni, 87.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16210820 total,   296972 free, 10072092 used,  5841756 buff/cache
KiB Swap: 16777212 total, 16777212 free,        0 used.  5480568 avail Mem
....

Aquí vemos que todos los núcleos se usan aproximadamente de la misma manera. (los indicadores us y sy e id son aproximadamente iguales para cada núcleo).

Ahora intentemos ver lo mismo con el comando del conjunto de tareas.

[user@localhost]# taskset -p 1
pid 1's current affinity mask: f

La máscara de bits "F" en el sistema hexadecimal significa 15 en decimal, o 1111 en binario (8 + 4 + 2 + 1). Todos los bits están habilitados, lo que significa que todos los núcleos son utilizados por un proceso con PID 1.
En Linux, cuando un proceso genera otro con una llamada al sistema de clonación, la máscara de bits se copia del padre en el momento de la clonación. Esto significa que si cambiamos esta máscara para nuestro proceso de inicio (en mi caso es systemd), al comenzar cualquier proceso nuevo a través de systemd, este nuevo proceso ya se iniciará con una nueva máscara.

Puede cambiar la máscara para el proceso utilizando el mismo comando, enumerando los números de núcleos de CPU que queremos dejar utilizados para el proceso. Supongamos que queremos dejar el kernel 0.2.3 para nuestro proceso, y queremos deshabilitar el kernel 1 para nuestro proceso systemd. Para hacer esto, necesitamos ejecutar el comando:

[user@localhost]#  taskset -pc 0,2,3 1
pid 1's current affinity list: 0-3
pid 1's new affinity list: 0,2,3

Verificamos:

[user@localhost]# taskset -p 1
pid 1's current affinity mask: d

La máscara cambió a "D" en la notación hexadecimal, que es 13 en decimal y 1101 en binario (8 + 4 + 0 + 1).

De ahora en adelante, cualquier proceso que será clonado por el proceso systemd tendrá automáticamente una máscara 1101 de uso de CPU, lo que significa que no se utilizará el núcleo número 1.

Prohibimos el uso del núcleo a todos los procesos.


Evitar que el proceso principal de Linux use un solo núcleo solo afectará los nuevos procesos creados por este proceso. Pero en mi sistema ya no hay un solo proceso, sino una multitud completa, como crond, sshd, bash y otros. Si necesito prohibir que todos los procesos usen un núcleo, entonces debo ejecutar el comando del conjunto de tareas para cada proceso en ejecución.

Para obtener una lista de todos los procesos, utilizaremos la API que nos proporciona el núcleo, es decir, el sistema de archivos / proc.

Más adelante en el bucle, observamos el PID de cada proceso en ejecución y cambiamos la máscara y todos los hilos:

[user@localhost]# cd /proc; for i in `ls -d [0-9]*`; do taskset -a -pc 0,2,3 $i; done
pid 1's current affinity list: 0,2,3
pid 1's new affinity list: 0,2,3
...

Dado que durante la ejecución del programa, algunos procesos podrían tener tiempo para generar otros procesos, es mejor ejecutar este comando varias veces.

Verifique el resultado de nuestro trabajo con el comando superior:

[user@localhost]# top
top - 14:20:46 up 2 days, 3 min,  7 users,  load average: 0.19, 0.27, 0.57
Tasks: 324 total,   4 running, 320 sleeping,   0 stopped,   0 zombie
%Cpu0  :  8.9 us,  7.7 sy,  0.0 ni, 83.4 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :  0.0 us,  0.0 sy,  0.0 ni,100.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  9.5 us,  6.0 sy,  0.0 ni, 84.5 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  :  8.4 us,  6.6 sy,  0.0 ni, 85.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 16210820 total,   285724 free, 10142548 used,  5782548 buff/cache
KiB Swap: 16777212 total, 16777212 free,        0 used.  5399648 avail Mem

Como puede ver, la imagen ha cambiado un poco, ahora para el kernel 0.2.3 los parámetros promedio us, sy, id son los mismos para nosotros, y para el kernel 1 nuestro consumo central en el espacio de usuario y sys es 0, y el kernel está inactivo al 100% (inactivo 100 ) El núcleo 1 ya no es utilizado por nuestras aplicaciones y el núcleo utiliza actualmente un porcentaje muy pequeño.

Ahora la tarea de probar el rendimiento se reduce a comenzar nuestro proceso en un núcleo libre.

Memoria


La memoria física asignada a un proceso puede extraerse fácilmente de cualquier proceso. Este mecanismo se llama intercambio. Si Linux tiene un lugar para el intercambio, lo hará de todos modos. La única forma de evitar que el sistema operativo tome memoria de nuestro proceso, como cualquier otro proceso, es deshabilitar completamente la sección de intercambio, lo que haremos:

[user@localhost]$ sudo swapoff -a
[user@localhost]$ free -m
              total        used        free      shared  buff/cache   available
Mem:          15830        7294        1894         360        6641        7746
Swap:             0           0           0

Asignamos 1 núcleo de procesador, que no se usa, y también eliminamos la capacidad de intercambiar memoria del kernel de Linux.

Disco


Para reducir el impacto del disco en el inicio de nuestro proceso, cree un disco en la memoria y copie todos los archivos necesarios en este disco.

Cree un directorio y monte el sistema de archivos:

[user@localhost]$ sudo mkdir /mnt/ramdisk;
[user@localhost]$ mount -t tmpfs -o size=512m tmpfs /mnt/ramdisk
[user@localhost]$ chown user: /mnt/ramdisk

Ahora tenemos que descubrir qué y cómo planeamos lanzarlo. Para ejecutar nuestro programa, primero necesitamos compilar nuestro código:

[user@localhost]$ javac OddEvenViaMod.java

Entonces necesitas ejecutarlo:

[user@localhost]$ java OddEvenViaMod

Pero en nuestro caso, queremos ejecutar el proceso en el núcleo del procesador que no es utilizado por ningún otro proceso. Por lo tanto, ejecútelo a través del conjunto de tareas:

[user@localhost]# taskset -c 1 time java OddEvenViaMod

En nuestras pruebas, necesitamos medir el tiempo, por lo que nuestra línea de lanzamiento se convierte en

taskset -c 1 time java OddEvenViaMod

El sistema operativo Linux admite varios formatos de archivos ejecutables, y el más común de ellos es el formato ELF. Este formato de archivo le permite decirle al sistema operativo que no ejecute su archivo, sino que ejecute otro archivo. A primera vista, no suena muy lógico y comprensible. Imagina que lanzo el juego Buscaminas, y el juego Mario comienza para mí, parece un virus. Pero esta es la lógica. Si mi programa requiere alguna biblioteca dinámica, por ejemplo, libc, o cualquier otra, esto significa que el sistema operativo primero debe cargar esta biblioteca en la memoria, y luego cargar y ejecutar mi programa. Y parece lógico colocar dicha funcionalidad en el sistema operativo en sí, pero el sistema operativo funciona en un área protegida de memoria y debe contener la menor funcionalidad posible y necesaria.Por lo tanto, el formato ELF brinda la oportunidad de decirle al sistema operativo que queremos descargar algún otro programa, y ​​este "otro" programa descargará todas las bibliotecas necesarias y nuestro programa y comenzará todo.

Entonces, necesitamos ejecutar 3 archivos, este es un conjunto de tareas, tiempo, java.

Comprueba el primero de ellos:

[user@localhost]$ whereis taskset
taskset: /usr/bin/taskset /usr/share/man/man1/taskset.1.gz

Bash ejecutará el archivo / usr / bin / taskset, verifique lo que hay dentro:

[user@localhost]$ file /usr/bin/taskset
/usr/bin/taskset: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=7a2fd0779f64aa9047faa00f498042f0f0c5dc60, stripped

Este es el archivo ELF sobre el que escribí anteriormente. En el archivo ELF, además del programa en sí, hay varios encabezados. Al iniciar este archivo, el sistema operativo verifica sus encabezados, y si el encabezado "Solicitar intérprete de programa" existe en el archivo, el sistema operativo iniciará el archivo desde este encabezado, y pasará el archivo lanzado inicialmente como argumento.

Compruebe si este encabezado existe en nuestro archivo ELF:

[user@localhost]$ readelf -a /usr/bin/taskset  | grep -i interpreter
      [Requesting program interpreter: /lib64/ld-linux-x86-64.so.2]

El encabezado existe, lo que significa que al iniciar el archivo / usr / bin / taskset en realidad ejecutamos /lib64/ld-linux-x86-64.so.2.

Comprueba qué es este archivo:

[user@localhost]$ ls -lah /lib64/ld-linux-x86-64.so.2
lrwxrwxrwx 1 root root 10 May 21  2019 /lib64/ld-linux-x86-64.so.2 -> ld-2.17.so

Este es un enlace sim al archivo /lib64/ld-2.17.so. Echale un vistazo:

[user@localhost]$ file /lib64/ld-2.17.so
/lib64/ld-2.17.so: ELF 64-bit LSB shared object, x86-64, version 1 (SYSV), dynamically linked, BuildID[sha1]=a527fe72908703c5972ae384e78d1850d1881ee7, not stripped

Como puede ver, este es otro archivo ELF que ejecutará el sistema operativo. Nos fijamos en los encabezados:

[user@localhost]$ readelf -a /lib64/ld-2.17.so  | grep -i interpreter
[user@localhost]$

Vemos que este archivo ELF no tiene ese encabezado, por lo que el sistema operativo ejecutará este archivo y le transferirá el control. Y ya este archivo abrirá nuestro archivo / usr / bin / taskset, lea desde allí la información sobre todas las bibliotecas necesarias. La lista de bibliotecas necesarias también se encuentra en los encabezados del archivo ELF. Podemos mirar esta lista con el comando ldd o readelf, que es lo mismo:

[user@localhost]$ ldd /usr/bin/taskset
	linux-vdso.so.1 =>  (0x00007ffc4c1df000)
	libc.so.6 => /lib64/libc.so.6 (0x00007f4a24c4e000)
	/lib64/ld-linux-x86-64.so.2 (0x00007f4a2501b000)

[user@localhost]$ readelf -a /usr/bin/taskset  | grep NEEDED
 0x0000000000000001 (NEEDED)             Shared library: [libc.so.6]

VDSO es una pieza de memoria vinculada que no está relacionada con las bibliotecas, por lo tanto, falta en el archivo ELF como una biblioteca necesaria.

Esto deja en claro que el programa /lib64/ld-2.17.so es responsable de ejecutar todos los programas que lo requieren, y todos estos son programas con bibliotecas vinculadas dinámicamente.

Si ejecutamos / usr / bin / taskset, esto es exactamente lo mismo que ejecutamos /lib64/ld-2.17.so con el argumento / usr / bin / taskset.

Volvemos al problema de la influencia del disco en nuestras pruebas. Ahora sabemos que si queremos cargar nuestro programa desde la memoria, entonces necesitamos copiar no un archivo, sino varios:

[user@localhost]$ cp /lib64/libc-2.17.so /mnt/ramdisk/
[user@localhost]$ cp /lib64/ld-2.17.so /mnt/ramdisk/
[user@localhost]$ cp /usr/bin/taskset /mnt/ramdisk/

Hacemos lo mismo por el tiempo, los requisitos de la biblioteca para los cuales son exactamente los mismos (ya copiamos ld y libc).

[user@localhost]$ cp /usr/bin/time /mnt/ramdisk/

Para Java, las cosas son un poco más complicadas, ya que Java requiere muchas bibliotecas diferentes que se pueden copiar durante mucho tiempo. Para simplificar un poco mi vida, copiaré todo el directorio de mi java openjdk a un disco en la memoria y crearé un enlace sim. Por supuesto, los accesos al disco permanecerán en este caso, pero habrá menos.

[user@localhost]$ cp -R /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64 /mnt/ramdisk/

Cambie el nombre del directorio anterior y agregue el final predeterminado.

[user@localhost]$ sudo mv /usr/lib/jvm/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64{,.default}

Y crea un enlace simbólico:

[user@localhost]$ sudo ln -s /mnt/ramdisk/java-1.8.0-openjdk-1.8.0.222.b10-0.el7_6.x86_64 /usr/lib/jvm/

Ya sabemos cómo ejecutar un archivo binario a través del argumento del archivo /lib64/ld-2.17.so, que en realidad comienza. Pero, ¿cómo hacer que el programa /lib64/ld-2.17.so cargue bibliotecas cargadas desde el directorio que especificamos? man ld para ayudarnos, de lo cual aprendemos que si declara la variable de entorno LD_LIBRARY_PATH, el programa ld cargará las bibliotecas de los directorios que especifiquemos. Ahora tenemos todos los datos para preparar la línea de lanzamiento de la aplicación Java.

Comenzamos varias veces seguidas y verificamos:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.66user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20344maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.65user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.66user 0.01system 0:10.68elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+64outputs (0major+5229minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.65user 0.01system 0:10.67elapsed 99%CPU (0avgtext+0avgdata 20348maxresident)k
0inputs+96outputs (0major+5234minor)pagefaults 0swaps
[user@localhost ramdisk]$

Durante la ejecución del programa, podemos ejecutar top y asegurarnos de que el programa se ejecute en el núcleo de CPU correcto.

[user@localhost ramdisk]$ top
...
%Cpu0  : 19.7 us, 11.7 sy,  0.0 ni, 68.6 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu1  :100.0 us,  0.0 sy,  0.0 ni,  0.0 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu2  :  9.8 us,  9.1 sy,  0.0 ni, 81.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
%Cpu3  : 14.0 us,  9.0 sy,  0.0 ni, 77.1 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 s
...

Como puede ver, los resultados en la mayoría de los casos son similares. Desafortunadamente, no podemos eliminar por completo la influencia del sistema operativo en el núcleo de la CPU, por lo que el resultado aún depende de las tareas específicas dentro del núcleo de Linux en el momento del lanzamiento. Por lo tanto, es mejor usar la mediana de los valores de varios inicios.

En nuestro caso, vemos que el programa Java procesa 9,000,000,000 con paridad a través del resto de la división en 10.65 segundos en un núcleo de CPU.

Hagamos la misma prueba con nuestro segundo programa, que hace lo mismo a través del binario AND.

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.02user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5197minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.01user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20228maxresident)k
0inputs+64outputs (0major+5199minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.01user 0.01system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5198minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so java OddEvenViaAnd
Odd 4500000000
Even 4500000000
4.02user 0.00system 0:04.03elapsed 99%CPU (0avgtext+0avgdata 20224maxresident)k
0inputs+64outputs (0major+5198minor)pagefaults 0swaps

Ahora podemos decir con confianza que la comparación de la paridad a través del binario Y toma 4.02 segundos, lo que significa que en comparación con el resto de la división, funciona 2.6 veces más rápido, al menos en la versión 1.8.0 de openjdk.

Oracle Java vs Openjdk


He descargado y descomprimido JDK de Java desde el sitio web de Oracle en el directorio /mnt/ramdisk/jdk-13.0.2.

Compilar:

[user@localhost ramdisk]$ /mnt/ramdisk/jdk-13.0.2/bin/javac OddEvenViaAnd.java

Lanzamos:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaAnd
Odd 4500000000
Even 4500000000
10.39user 0.01system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 24260maxresident)k
0inputs+64outputs (0major+6979minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaAnd
Odd 4500000000
Even 4500000000
10.40user 0.01system 0:10.42elapsed 99%CPU (0avgtext+0avgdata 24268maxresident)k
0inputs+64outputs (0major+6985minor)pagefaults 0swaps

Compilamos el segundo programa:

[user@localhost ramdisk]$ /mnt/ramdisk/jdk-13.0.2/bin/javac OddEvenViaMod.java

Lanzamos:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.39user 0.01system 0:10.40elapsed 99%CPU (0avgtext+0avgdata 24324maxresident)k
0inputs+96outputs (0major+7003minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/jdk-13.0.2/bin/java OddEvenViaMod
Odd 4500000000
Even 4500000000
10.40user 0.00system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 24316maxresident)k
0inputs+64outputs (0major+6992minor)pagefaults 0swaps

El tiempo de ejecución de las mismas fuentes en Oracle jdk es el mismo para el resto de la división y el binario AND, que parece normal, pero esta vez es igualmente malo, lo que se mostró en openjdk en el resto de la división.

Pitón


Intentemos comparar lo mismo en Python. Primero, la opción con el resto de dividir por 2:

odd=0
even=0
for i in xrange(100000000):
	if i % 2 == 0:
		even += 1
	else:
		odd += 1
print "even", even
print "odd", odd

Lanzamos:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.69user 0.00system 0:11.69elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.67user 0.00system 0:11.67elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_mod.py
even 50000000
odd 50000000
11.66user 0.00system 0:11.66elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1220minor)pagefaults 0swaps

Ahora lo mismo con binario AND:

odd=0
even=0
for i in xrange(100000000):
	if i & 1 == 0:
		even += 1
	else:
		odd += 1
print "even", even
print "odd", odd

Lanzamos:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.41user 0.00system 0:10.41elapsed 99%CPU (0avgtext+0avgdata 4588maxresident)k
0inputs+0outputs (0major+1221minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.43user 0.00system 0:10.43elapsed 99%CPU (0avgtext+0avgdata 4588maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and.py
even 50000000
odd 50000000
10.43user 0.00system 0:10.43elapsed 99%CPU (0avgtext+0avgdata 4584maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps

Los resultados muestran que Y es más rápido.

En Internet, se ha escrito muchas veces que las variables globales en Python son más lentas. Decidí comparar el tiempo de ejecución del último programa con AND y exactamente el mismo, pero envuelto en una función:

def main():
	odd=0
	even=0
	for i in xrange(100000000):
		if i & 1 == 0:
			even += 1
		else:
			odd += 1
	print "even", even
	print "odd", odd

main()

Ejecutar en la función:

[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and_func.py
even 50000000
odd 50000000
5.08user 0.00system 0:05.08elapsed 99%CPU (0avgtext+0avgdata 4592maxresident)k
0inputs+0outputs (0major+1222minor)pagefaults 0swaps
[user@localhost ramdisk]$
[user@localhost ramdisk]$ export LD_LIBRARY_PATH=/mnt/ramdisk ; /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/taskset -c 1 /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/time /mnt/ramdisk/ld-2.17.so /mnt/ramdisk/python2.7 odd_and_func.py
even 50000000
odd 50000000
5.08user 0.00system 0:05.09elapsed 99%CPU (0avgtext+0avgdata 4592maxresident)k
0inputs+0outputs (0major+1223minor)pagefaults 0swaps

Como puede ver, la misma comparación de paridad en Python a través de AND binario en una función procesa 100000000 números en un solo núcleo de CPU en ~ 5 segundos, la misma comparación a través de AND sin una función toma ~ 10 segundos, y la comparación sin una función en el resto de la división toma ~ 11 segundos

Por qué un programa Python en una función funciona más rápido que sin él ya se ha descrito más de una vez y está relacionado con el alcance de las variables.

Python tiene la capacidad de desarmar un programa en funciones internas que Python utiliza al interpretar un programa. Veamos qué funciones usa Python para la variante con la función odd_and_func.py:

[user@localhost ramdisk]# python
Python 2.7.5 (default, Jun 20 2019, 20:27:34)
[GCC 4.8.5 20150623 (Red Hat 4.8.5-36)] on linux2
Type "help", "copyright", "credits" or "license" for more information.
>>> def main():
...     odd=0
...     even=0
...     for i in xrange(100000000):
...             if i & 1 == 0:
...                     even += 1
...             else:
...                     odd += 1
...     print "even", even
...     print "odd", odd
...
>>> import dis
>>> dis.dis(main)
  2           0 LOAD_CONST               1 (0)
              3 STORE_FAST               0 (odd)

  3           6 LOAD_CONST               1 (0)
              9 STORE_FAST               1 (even)

  4          12 SETUP_LOOP              59 (to 74)
             15 LOAD_GLOBAL              0 (xrange)
             18 LOAD_CONST               2 (100000000)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                45 (to 73)
             28 STORE_FAST               2 (i)

  5          31 LOAD_FAST                2 (i)
             34 LOAD_CONST               3 (1)
             37 BINARY_AND
             38 LOAD_CONST               1 (0)
             41 COMPARE_OP               2 (==)
             44 POP_JUMP_IF_FALSE       60

  6          47 LOAD_FAST                1 (even)
             50 LOAD_CONST               3 (1)
             53 INPLACE_ADD
             54 STORE_FAST               1 (even)
             57 JUMP_ABSOLUTE           25

  8     >>   60 LOAD_FAST                0 (odd)
             63 LOAD_CONST               3 (1)
             66 INPLACE_ADD
             67 STORE_FAST               0 (odd)
             70 JUMP_ABSOLUTE           25
        >>   73 POP_BLOCK

  9     >>   74 LOAD_CONST               4 ('even')
             77 PRINT_ITEM
             78 LOAD_FAST                1 (even)
             81 PRINT_ITEM
             82 PRINT_NEWLINE

 10          83 LOAD_CONST               5 ('odd')
             86 PRINT_ITEM
             87 LOAD_FAST                0 (odd)
             90 PRINT_ITEM
             91 PRINT_NEWLINE
             92 LOAD_CONST               0 (None)
             95 RETURN_VALUE

Y verifique lo mismo sin usar la función en nuestro código:

>>> f=open("odd_and.py","r")
>>> l=f.read()
>>>
>>> l
'odd=0\neven=0\nfor i in xrange(100000000):\n\tif i & 1 == 0:\n\t\teven += 1\n\telse:\n\t\todd += 1\nprint "even", even\nprint "odd", odd\n'
>>> k=compile(l,'l','exec')
>>> k
<code object <module> at 0x7f2bdf39ecb0, file "l", line 1>
>>> dis.dis(k)
  1           0 LOAD_CONST               0 (0)
              3 STORE_NAME               0 (odd)

  2           6 LOAD_CONST               0 (0)
              9 STORE_NAME               1 (even)

  3          12 SETUP_LOOP              59 (to 74)
             15 LOAD_NAME                2 (xrange)
             18 LOAD_CONST               1 (100000000)
             21 CALL_FUNCTION            1
             24 GET_ITER
        >>   25 FOR_ITER                45 (to 73)
             28 STORE_NAME               3 (i)

  4          31 LOAD_NAME                3 (i)
             34 LOAD_CONST               2 (1)
             37 BINARY_AND
             38 LOAD_CONST               0 (0)
             41 COMPARE_OP               2 (==)
             44 POP_JUMP_IF_FALSE       60

  5          47 LOAD_NAME                1 (even)
             50 LOAD_CONST               2 (1)
             53 INPLACE_ADD
             54 STORE_NAME               1 (even)
             57 JUMP_ABSOLUTE           25

  7     >>   60 LOAD_NAME                0 (odd)
             63 LOAD_CONST               2 (1)
             66 INPLACE_ADD
             67 STORE_NAME               0 (odd)
             70 JUMP_ABSOLUTE           25
        >>   73 POP_BLOCK

  8     >>   74 LOAD_CONST               3 ('even')
             77 PRINT_ITEM
             78 LOAD_NAME                1 (even)
             81 PRINT_ITEM
             82 PRINT_NEWLINE

  9          83 LOAD_CONST               4 ('odd')
             86 PRINT_ITEM
             87 LOAD_NAME                0 (odd)
             90 PRINT_ITEM
             91 PRINT_NEWLINE
             92 LOAD_CONST               5 (None)
             95 RETURN_VALUE

Como puede ver, en la variante con la función declarada, Python usa funciones internas con el postfix FAST, por ejemplo, STORE_FAST, LOAD_FAST, y en la variante sin la declaración de la función, Python usa las funciones internas STORE_NAME y LOAD_NAME.

Este artículo tiene poco significado práctico y está dirigido más a comprender algunas de las características de Linux y los compiladores.

Bueno para todos!

All Articles