Teste de desempenho de código do Linux com exemplos

Quando comecei a aprender Java, uma das primeiras tarefas que tentei resolver foi determinar números pares / ímpares. Eu conhecia várias maneiras de fazer isso, mas decidi procurar o caminho "certo" na Internet. As informações em todos os links encontrados me informavam sobre a única solução correta do formulário x% 2, para obter o restante da divisão. Se o restante for 0, o número será par; se o restante for 1, será ímpar.

Desde a época do ZX Spectrum, lembrei-me de outra maneira e ela está associada à representação de números no sistema binário. Qualquer número no sistema decimal pode ser escrito como a soma dos poderes de dois. Por exemplo, para um byte, e este é 8 bits, qualquer número no sistema decimal pode ser representado como a soma dos números 128 + 64 + 32 + 16 + 8 + 4 + 2 + 1.

Esta é apenas uma sequência de poderes de dois. Ao converter um número no sistema binário, se precisarmos levar em conta o número, na representação binária será um, se não for necessário, será 0.

Por exemplo:

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

Você pode prestar atenção imediatamente ao fato de que um número ímpar pode fornecer apenas uma potência zero de dois com um valor de 1; todos os outros poderes de dois serão pares por definição. Isso significa automaticamente que, do ponto de vista do sistema de números binários, se quisermos verificar qualquer número quanto à paridade, não precisamos verificar o número inteiro, por maior que seja. Precisamos verificar apenas o primeiro bit (o mais à direita). Se for 0, então o número é par, já que todos os outros bits fornecem um número par e vice-versa, se for um no bit mais à direita, é garantido que o número seja ímpar, porque todos os outros bits fornecem apenas um valor par.
Para verificar apenas o bit certo em um número, você pode usar vários métodos. Um deles é o binário AND.

E


O binário AND (AND) funciona pela seguinte regra. Se você aplicar a qualquer número, vamos chamá-lo de original, lógico E com o número 0, então o resultado dessa operação é sempre 0. Dessa maneira, você pode zerar os bits que não precisa. Se você aplicar ao original 1, obterá o original.

Em um sistema binário, é fácil escrever isso:

0 AND 0 = 0 (zere o original)
1 AND 0 = 0 (zere o original)
0 AND 1 = 0 (não mude o original)
1 AND 1 = 1 (não mude o original)

A partir daqui, alguns exemplos regras.

Se aplicarmos a operação AND de todas as unidades a todos os números (todos os bits estão ativados), obteremos o mesmo número inicial.

Se aplicarmos AND de todos os zeros a qualquer número (todos os bits estão desativados), obtemos 0.

Por exemplo:

Se aplicarmos AND 0 ao byte 13, obteremos 0. Em decimal, pareceremos 13 AND 0 = 0

Se aplicarmos AND 0 ao byte 200, obteremos 0 ou escreveremos brevemente 200 AND 0 = 0.
O mesmo é o oposto, aplicar a 13 todos os bits incluídos, para um byte serão oito unidades e obteremos o original. No sistema binário 00001101 AND 11111111 = 00001101 ou no sistema decimal 13 AND 255 = 13

Para 200, haverá 11001000 AND 11111111 = 11001000, respectivamente, ou no sistema decimal 200 AND 255 = 200

Verificação binária


Para verificar o número de paridade, precisamos apenas verificar o bit mais à direita. Se for 0, o número será par; se 1, não será par. Sabendo que com AND podemos deixar alguns bits originais, e alguns podemos redefinir, podemos redefinir todos os bits, exceto o mais à direita. Por exemplo:

13 no sistema binário é 1101. Vamos aplicar AND 0001 a ele (redefinimos todos os bits, o último permanece o original). Em 1101, alteramos todos os bits para 0, exceto o último, e obtemos 0001. Obtivemos apenas o último bit do nosso número original. No sistema decimal, será semelhante a 13 AND 1 = 1.

A mesma coisa com o número 200, no binário 11001000. Aplicamos AND 00000001 a ele, de acordo com o mesmo esquema, zeramos todos os bits, deixamos o último como está, obtemos 00000000, enquanto os 7 zeros à esquerda foram redefinidos com o comando AND e os últimos 0 obtivemos do número original. No sistema decimal, parece 200 AND 1 = 0

Assim, aplicando o comando AND 1 a qualquer número, obtemos 0 ou 1. E se o resultado for 0, o número será par. Quando 1, o número é ímpar.

Em Java, o binário AND é escrito como &. Assim, 200 & 1 = 0 (par) e 13 & 1 = 1 (ímpar).

Isso implica pelo menos dois métodos para determinar números pares.

X% 2 - através do restante da divisão por dois
X e 1 - através de AND binário

Operações binárias como OR, AND, XOR são processadas pelo processador em um período mínimo de tempo. Mas a operação de divisão é uma tarefa não trivial e, para executá-la, o processador precisa processar muitas instruções, essencialmente executar todo o programa. No entanto, existem operações binárias de deslocamento à esquerda e à direita que permitem, por exemplo, dividir rapidamente um número por 2. A questão é se os compiladores usam essa otimização e se há uma diferença entre essas duas comparações, que de fato fazem o mesmo.

Codificação


Escreveremos um programa que processará 9.000.000.000 de números em um ciclo em ordem e determinaremos sua pertença a par / ímpar determinando o restante da divisão.

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);
        }
}

E escreveremos exatamente o mesmo, mas literalmente mudamos dois caracteres, verificando a mesma coisa através de um binário 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);

Agora, precisamos comparar de alguma forma esses dois programas.

Recursos no Linux. CPU


Uma quantidade enorme de horas foi gasta na criação de qualquer sistema operacional, em particular na distribuição justa de recursos entre os programas. Por um lado, isso é bom, pois ao executar dois programas, você pode ter certeza de que eles funcionarão em paralelo, mas por outro lado, quando você precisa verificar o desempenho de um programa, é extremamente necessário limitar ou pelo menos reduzir a influência externa sobre o programa de outras pessoas. programas e sistema operacional.

A primeira coisa a descobrir é o processador. O SO Linux para cada processo armazena uma máscara de bits, que indica quais kernels podem ser usados ​​pelo aplicativo e quais não. Você pode visualizar e alterar essa máscara com o comando taskset.

Por exemplo, vamos ver o número de núcleos no meu processador:

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

Meu computador possui um processador com 4 núcleos. Isso é bom, porque vou alocar um deles para minhas necessidades.

Vamos ver se todos eles estão em uso no momento com o comando top:

[user@localhost]# top

Pressione "1" para visualizar informações sobre cada núcleo separadamente:

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
....

Aqui vemos que todos os núcleos são usados ​​aproximadamente da mesma forma. (os indicadores us e sy e id são aproximadamente iguais para cada núcleo).

Agora vamos tentar ver o mesmo com o comando taskset.

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

A máscara de bit "F" no sistema hexadecimal significa 15 em decimal ou 1111 em binário (8 + 4 + 2 + 1). Todos os bits estão ativados, o que significa que todos os núcleos são usados ​​por um processo com PID 1.
No Linux, quando um processo gera outro com uma chamada de sistema clone, a máscara de bit é copiada do pai no momento da clonagem. Isso significa que, se alterarmos essa máscara para o nosso processo init (no meu caso, é systemd), ao iniciar qualquer novo processo através do systemd, esse novo processo já será iniciado com uma nova máscara.

Você pode alterar a máscara do processo usando o mesmo comando, listando os números de núcleos da CPU que queremos deixar usados ​​para o processo. Suponha que desejemos deixar o kernel 0.2.3 para o nosso processo e queremos desativar o kernel 1 para o processo do systemd. Para fazer isso, precisamos executar o 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

A máscara mudou para "D" na notação hexadecimal, que é 13 em decimal e 1101 em binário (8 + 4 + 0 + 1).

A partir de agora, qualquer processo que será clonado pelo processo systemd terá automaticamente uma máscara 1101 de uso da CPU, o que significa que o kernel número 1 não será usado.

Proibimos o uso do kernel em todos os processos


Impedir que o processo principal do Linux use um único kernel afetará apenas os novos processos criados por esse processo. Mas no meu sistema já não existe um processo, mas uma multidão inteira, como crond, sshd, bash e outros. Se eu precisar proibir todos os processos de usar um núcleo, execute o comando taskset para cada processo em execução.

Para obter uma lista de todos os processos, usaremos a API que o kernel nos fornece, a saber, o sistema de arquivos / proc.

Mais adiante, analisamos o PID de cada processo em execução e alteramos a máscara para ele e todos os threads:

[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
...

Como durante a execução do programa, alguns processos podem ter tempo para gerar outros processos, é melhor executar esse comando várias vezes.

Verifique o resultado do nosso trabalho com o comando top:

[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 você pode ver, a imagem mudou um pouco, agora para o kernel 0.2.3 os parâmetros médios us, sy, id são os mesmos para nós, e para o kernel 1 nosso consumo principal no espaço do usuário e sys é 0 e o kernel está ocioso a 100% (ocioso 100 ) O kernel 1 não é mais usado por nossos aplicativos e uma porcentagem muito pequena atualmente é usada pelo kernel.

Agora, a tarefa de testar o desempenho é reduzida para iniciar nosso processo em um núcleo livre.

Memória


A memória física alocada para um processo pode ser facilmente obtida de qualquer processo. Esse mecanismo é chamado de troca. Se o Linux tiver um lugar para troca, ele será feito de qualquer maneira. A única maneira de impedir que o sistema operacional tire memória do nosso processo, como qualquer outro processo, é desativar completamente a seção de troca, o que faremos:

[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

Alocamos 1 núcleo do processador, que não é usado, e também removemos a capacidade de trocar memória do kernel do Linux.

Disco


Para reduzir o impacto do disco no lançamento do nosso processo, crie um disco na memória e copie todos os arquivos necessários para esse disco.

Crie um diretório e monte o sistema de arquivos:

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

Agora precisamos descobrir o que e como planejamos lançá-lo. Para executar nosso programa, primeiro precisamos compilar nosso código:

[user@localhost]$ javac OddEvenViaMod.java

Então você precisa executá-lo:

[user@localhost]$ java OddEvenViaMod

Mas, no nosso caso, queremos executar o processo no núcleo do processador que não é usado por nenhum outro processo. Portanto, execute-o no conjunto de tarefas:

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

Em nossos testes, precisamos medir o tempo, para que nossa linha de lançamento se transforme em

taskset -c 1 time java OddEvenViaMod

O SO Linux suporta vários formatos de arquivos executáveis, e o mais comum deles é o formato ELF. Esse formato de arquivo permite que o sistema operacional não execute seu arquivo, mas execute outro arquivo. À primeira vista, não parece muito lógico e compreensível. Imagine que eu inicio o jogo Campo Minado, e o jogo Mario começa para mim - parece um vírus. Mas essa é a lógica. Se o meu programa exigir qualquer biblioteca dinâmica, por exemplo libc ou qualquer outra, isso significa que o sistema operacional deve primeiro carregar essa biblioteca na memória e depois carregar e executar o meu programa. E parece lógico colocar essa funcionalidade no próprio sistema operacional, mas o sistema operacional funciona em uma área protegida da memória e deve conter o mínimo de funcionalidade possível e necessário.Portanto, o formato ELF oferece a oportunidade de informar ao sistema operacional que queremos fazer o download de algum outro programa, e esse "outro" programa fará o download de todas as bibliotecas necessárias e do nosso programa e iniciará tudo.

Então, precisamos executar 3 arquivos, este é o conjunto de tarefas, hora, java.

Confira o primeiro deles:

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

O Bash executará o arquivo / usr / bin / taskset, verifique o que está 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 é o arquivo ELF que escrevi acima. No arquivo ELF, além do próprio programa, existem vários cabeçalhos. Ao iniciar esse arquivo, o sistema operacional verifica seus cabeçalhos e, se o cabeçalho "Solicitando intérprete de programa" existir no arquivo, o sistema operacional iniciará o arquivo a partir desse cabeçalho e passará o arquivo iniciado inicialmente como argumento.

Verifique se este cabeçalho existe em nosso arquivo ELF:

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

O cabeçalho existe, o que significa que, ao iniciar o arquivo / usr / bin / taskset, executamos /lib64/ld-linux-x86-64.so.2.

Verifique o que é esse arquivo:

[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 é um link sim para o arquivo /lib64/ld-2.17.so. Confira:

[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 você pode ver, esse é outro arquivo ELF que o sistema operacional executará. Nós olhamos para os cabeçalhos:

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

Vemos que esse arquivo ELF não possui esse cabeçalho; portanto, o SO executará esse arquivo e transferirá o controle para ele. E esse arquivo já abrirá nosso arquivo / usr / bin / taskset, leia a partir daí informações sobre todas as bibliotecas necessárias. A lista de bibliotecas necessárias também está nos cabeçalhos do arquivo ELF. Podemos ver esta lista com o comando ldd ou readelf, que é a mesma coisa:

[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]

O VDSO é uma parte da memória vinculada que não está relacionada às bibliotecas; portanto, está ausente no arquivo ELF como uma biblioteca necessária.

Isso deixa claro que o programa /lib64/ld-2.17.so é responsável por executar todos os programas que o exigem, e todos esses programas com bibliotecas vinculadas dinamicamente.

Se executarmos / usr / bin / taskset, será exatamente o mesmo que executarmos /lib64/ld-2.17.so com o argumento / usr / bin / taskset.

Voltamos ao problema da influência do disco em nossos testes. Agora sabemos que, se queremos carregar nosso programa da memória, precisamos copiar não um arquivo, mas vários:

[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/

Fazemos o mesmo com o tempo, cujos requisitos de biblioteca são exatamente os mesmos (já copiámos ld e libc).

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

Para java, as coisas são um pouco mais complicadas, pois o java requer muitas bibliotecas diferentes que podem ser copiadas por um longo tempo. Para simplificar um pouco a minha vida, copiarei o diretório inteiro do meu java openjdk para um disco na memória e criei um link sim. Obviamente, os acessos ao disco permanecerão nesse caso, mas haverá 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/

Renomeie o diretório antigo, adicionando o final. Padrão a ele

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

E crie um link 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/

Já sabemos como executar um arquivo binário através do argumento para o arquivo /lib64/ld-2.17.so, que realmente é iniciado. Mas como fazer com que o programa /lib64/ld-2.17.so carregue bibliotecas carregadas do diretório especificado? man ld para nos ajudar, a partir do qual aprendemos que, se você declarar a variável de ambiente LD_LIBRARY_PATH, o programa ld carregará as bibliotecas dos diretórios especificados. Agora, temos todos os dados para preparar a linha de lançamento do aplicativo Java.

Começamos várias vezes seguidas e 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 a execução do programa, podemos executar o top e garantir que o programa seja executado no núcleo da CPU correto.

[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 você pode ver, os resultados na maioria dos casos são semelhantes. Infelizmente, não podemos remover completamente a influência do sistema operacional no núcleo da CPU, portanto o resultado ainda depende das tarefas específicas dentro do kernel do Linux no momento do lançamento. Portanto, é melhor usar a mediana dos valores de várias partidas.

No nosso caso, vemos que o programa java processa 9.000.000.000 com paridade pelo restante da divisão em 10.65 segundos em um núcleo de CPU.

Vamos fazer o mesmo teste com o nosso segundo programa, que faz a mesma coisa através do AND binário.

[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

Agora podemos dizer com confiança que a comparação da paridade através do binário AND leva 4,02 segundos, o que significa que, comparada à verificação no restante da divisão, ela funciona 2,6 vezes mais rápido, pelo menos no openjdk versão 1.8.0.

Oracle Java vs Openjdk


Eu baixei e jdk java descompactado no site da Oracle para o diretório /mnt/ramdisk/jdk-13.0.2.

Compilação:

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

Lançamos:

[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

Nós compilamos o segundo programa:

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

Lançamos:

[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

O tempo de execução das mesmas fontes no oracle jdk é o mesmo para o restante da divisão e o binário AND, que parece normal, mas esse tempo é igualmente ruim, o que foi mostrado em openjdk no restante da divisão.

Pitão


Vamos tentar comparar o mesmo em Python. Primeiro, a opção com o restante da divisão 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

Lançamos:

[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

Agora, a mesma coisa com o binário 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

Lançamos:

[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

Os resultados mostram que AND é mais rápido.

Na Internet, já foi escrito muitas vezes que as variáveis ​​globais em Python são mais lentas. Decidi comparar o tempo de execução do último programa com AND e exatamente o mesmo, mas envolvido em uma função:

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()

Execute na função:

[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 você pode ver, a mesma comparação de paridade no Python via AND binário em uma função processa 100000000 números em um único núcleo da CPU em ~ 5 segundos, a mesma comparação via AND sem uma função leva ~ 10 segundos, e a comparação sem uma função pelo restante da divisão leva ~ 11 segundos

Por que um programa Python em uma função funciona mais rápido do que sem ele já foi descrito mais de uma vez e está relacionado ao escopo das variáveis.

O Python tem a capacidade de desmontar um programa em funções internas que o Python usa ao interpretar um programa. Vamos ver quais funções o Python usa para a variante com a função 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

E verifique o mesmo sem usar a função em nosso 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 você pode ver, na variante com a função declarada, o Python usa funções internas com o postfix FAST, por exemplo, STORE_FAST, LOAD_FAST, e na variante sem a declaração da função, o Python usa as funções internas STORE_NAME e LOAD_NAME.

Este artigo tem pouco significado prático e visa mais entender alguns dos recursos do Linux e dos compiladores.

Bom para todos!

All Articles