Iniciando com Intel VTune
O objetivo deste artigo é apresentar o Intel® VTune™ Amplifier XE 2013 e mostrar um exemplo básico de seu funcionamento, tendo como base um código Java, o qual terá uma versão serial e uma versão com processamento paralelo, mostrando um caso prático de tunning de performance.
Não será utilizada aqui nenhuma técnica de programação avançada em Java para implementação de paralelismo e não analisaremos profundamente os relatórios gerados pelo VTune.
Requisitos
Todos os requisitos devem estar baixados e configurados. Este artigo foi escrito no sistema operacional Linux.
Java JDK versão 6 ou superior
Intel® VTune™ Amplifier XE 2013
Eclipse (ou seu ambiente de desenvolvimento Java preferido)
Introdução
VTune é um profiler de código paralelo ou serial, isto é, um software que analisa o desempenho de um programa e mostra informações importantes acerca de seu desempenho, como por exemplo, o uso do processador, os possíveis gargalos, entre outros.
Por tratar-se de um analisador de desempenho, o mesmo não pode ser utilizado em um ambiente virtual e necessita um processador Intel Genuíno.
Atualmente o VTune suporta as linguagens C, C++, C#, Fortran, Assembly e Java.
Por que usar?
É fato que as aplicações devem sempre extrair o máximo de recursos do Hardware disponível no ambiente, porém, muitos gargalos podem passar despercebidos. A utilização de uma ferramenta de monitoração de performance nos permite localizar tais gargalos e os eliminar, tornando nossa aplicações mais eficientes.
Preparação
O Java versão superior a 6 e o Eclipse devem estar instalados em sua máquina, feito isso, parta para a instalação do VTune. Este artigo não cobre a instalação do VTune e nem dos demais requisitos por estar disponível extenso material na internet sobre o assunto.
Para fins de profiling, alguns parâmetros do sistema operacional devem ser alterados, então, ao final da instalação do VTune, normalmente é exibida uma caixa de diálogo informando quais são estes parâmetros, como por exemplo o ptrace_scope, que para ser desabilitado deve-se executar como root num terminal o seguinte comando antes de executar o VTune:
echo 0 | tee /proc/sys/kernel/yama/ptrace_scope
Baixaremos ainda 1 imagem do DVD de instalação do Ubuntu (que pode ser baixada do site citado em referências). Em nosso exemplo, colocaremos a imagem no diretório /opt/isos/ e criaremos 3 copias da mesma. Lembre-se de trocar o nome dos arquivos caso baixe versões diferentes e além disso, dar permissão de leitura/escrita nos mesmos:
juliano@julianom:/opt/isos$ pwd
/opt/isos
juliano@julianom:/opt/isos$ ls
kubuntu-13.04-desktop-amd64_2.iso kubuntu-13.04-desktop-amd64.iso
kubuntu-13.04-desktop-amd64_4.iso kubuntu-13.04-desktop-amd64_3.iso
Entendendo a aplicação teste
A aplicação Java teste que iremos executar, criptografa 4 imagens da instalação do Ubuntu citadas anteriormente. Escolhemos o processo de criptografia por se tratar de algo pesado para o processador, e dessa forma, demandar um bom tempo do mesmo.
O algoritmo de criptografia utilizado é o BlowFish. Você pode substituir este trecho de código por qualquer processo que desejar, como por exemplo, adicionar uma Thread sleep, baixar algum arquivo da internet, entre outros, porém, recomendo que seja algo que faça operações de I/O para obter resultados mais didáticos no VTune.
Na primeira abordagem, efetuaremos a criptografia em série, ou seja, imagem por imagem vamos criptografando sequencialmente.
Escrevendo código
Criamos um projeto no Eclipse chamado VTuneTest:
Criamos uma classe chamada CryptoEngine, que será o coração de nosso sistema, responsável pela criptografia das imagens. Esta classe estará no pacote br.com.infoserver.cryptotester.impl e tem o seguinte conteúdo:
package br.com.infoserver.cryptotester.impl;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.security.Key;
import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.SecretKeySpec;
public class CryptoEngine implements Runnable {
private String fileName;
private byte iv[];
private byte[] key;
public CryptoEngine(String fileName, byte iv[], byte[] key){
this.fileName = fileName;
this.iv = iv;
this.key = key;
}
@Override
public void run() {
try {
encrypt(fileName, iv, key);
} catch (Exception e) {
e.printStackTrace();
}
}
public void encrypt(String fileName, byte iv[], byte[] key) throws Exception{
Key secretKey = new SecretKeySpec(key, "Blowfish");
Cipher cipherOut = Cipher.getInstance("Blowfish/CFB/NoPadding");
cipherOut.init(Cipher.ENCRYPT_MODE, secretKey);
FileInputStream fin = new FileInputStream(fileName);
FileOutputStream fout = new FileOutputStream(fileName+".enc");
CipherOutputStream cout = new CipherOutputStream(fout, cipherOut);
int input = 0;
while ((input = fin.read()) != -1) {
cout.write(input);
}
fin.close();
cout.close();
}
}
Criaremos a classe principal de nosso sistema, chamada RunCrypto, dentro do pacote X, com o seguinte conteúdo:
package br.com.infoserver.cryptotester.runtime;
import java.security.Key;
import java.util.ArrayList;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import br.com.infoserver.cryptotester.impl.CryptoEngine;
public class RunCrypto {
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
byte[] iv = new byte[8];
byte[] key = new byte[16];
key[0] = (byte) 150;
key[1] = 56;
key[2] = 47;
key[3] = (byte) 134;
key[4] = (byte) 227;
key[5] = 62;
key[6] = 74;
key[7] = 102;
key[8] = (byte) 178;
key[9] = (byte) 156;
key[10] = 45;
key[11] = 99;
key[12] = (byte) 222;
key[13] = (byte) 212;
key[14] = 84;
key[15] = (byte) 190;
iv[0] = (byte) 238;
iv[1] = 25;
iv[2] = (byte) 170;
iv[3] = 1;
iv[4] = (byte) 227;
iv[5] = 70;
iv[6] = 47;
iv[7] = 70;
KeyGenerator keyGenerator = KeyGenerator.getInstance("Blowfish");
keyGenerator.init(128);
Key secretKey = keyGenerator.generateKey();
Cipher cipherOut = Cipher.getInstance("Blowfish/CFB/NoPadding");
cipherOut.init(Cipher.ENCRYPT_MODE, secretKey);
ArrayList<String> fileNames = new ArrayList<String>();
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64.iso");
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64_2.iso");
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64_3.iso");
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64_4.iso");
// Serial
for (int i = 0; i < fileNames.size(); i++) {
System.out.println("Starting encryption: " + i);
CryptoEngine cEnginge = new CryptoEngine(fileNames.get(i), iv, key);
cEnginge.run();
}
final long stopTime = System.currentTimeMillis();
float ttime = ((stopTime - startTime) / 1000);
System.out.println("Total execution time: " + ttime + " seconds");
System.out.println("Total execution time: " + ttime / 60 + " minutes");
}
}
Feito isso, nosso programa que criptografa as imagens pode ser testando. Basta rodar e ir acompanhando a encriptação tomar forma no diretório /opt/iso. O processo é lento. Ao final do processo, você deverá ver 4 arquivos criados na pasta iso, que correspondem aos arquivos criptografados:
juliano@julianom:/opt/isos$ ls
kubuntu-13.04-desktop-amd64_2.iso kubuntu-13.04-desktop-amd64_3.iso.enc kubuntu-13.04-desktop-amd64.iso
kubuntu-13.04-desktop-amd64_2.iso.enc kubuntu-13.04-desktop-amd64_4.iso kubuntu-13.04-desktop-amd64.iso.enc
kubuntu-13.04-desktop-amd64_3.iso kubuntu-13.04-desktop-amd64_4.iso.enc
Como o objetivo é testar no VTune, é necessário gerar um runnable jar file. Para isso, clique no projeto e vá em Exportar, seleciona runnable jar file, chamarei o Jar file de RunCryptoSeq.jar.
Executando teste no VTune
Para poder executar a aplicação pelo VTune, você precisa criar um script que chama o jar criado anteriormente, tal script será invocado pelo VTune.
O script criado tem o seguinte nome RunCrypto.sh e o conteúdo abaixo:
#!/bin/bash
java -jar RunCryptoSeq.jar
Lembre-se de tornar o mesmo executável com o comando abaixo:
chmod +x RunCryptoSeq.jar
Criado o script, inicie o VTune:
Criaremos um projeto chamado JavaTest:
No input box Application, devemos apontar para o script criado:
Com o projeto criado, clicamos em New Analysis e Start. Irá levar um bom tempo, quando concluído, obteremos a tela abaixo:
Este gráfico é muito importante para nossa primeira análise, ele demonstra claramente que nossa aplicação não está aproveitando em nada a capacidade de processamento paralelo de nosso processador:
As visões geradas pelo VTune são muito completas e serão aprofundadas nos próximos artigos desta série. Basicamente nesta visualização estamos observando o tempo gasto por função de nossa aplicação:
Uma breve análise sem aprofundar-se nos relatórios do VTune, já deu a entender a qualquer um que nossa aplicação poderia estar fazendo melhor uso dos múltiplos cores disponíveis em nosso processador, implementando paralelismo!
É fato que o processamento paralelo deve ser muito bem analisado antes de implementado em uma solução, até mesmo, sua possibilidade, questões de negócio podem impedir sua utilização.
Caso você esteja trabalhando com outra linguagem que não seja Java, vale a pena pesquisar os recursos disponíveis para implementar paralelismo em seu fonte.
Reescrevendo o código
Em nosso exemplo, não seria mais inteligente lançar todas as criptografias simultaneamente e deixar elas serem alocadas para os cores disponíveis? Note que uma criptografia não depende da criptografia anterior, são processos independentes. Então, vamos reescrever o código para trabalhar com Threads, veja como nosso código ficou:
package br.com.infoserver.cryptotester.runtime;
import java.security.Key;
import java.util.ArrayList;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import br.com.infoserver.cryptotester.impl.CryptoEngine;
public class RunCryptoJCA {
public static void main(String[] args) throws Exception {
final long startTime = System.currentTimeMillis();
byte[] iv = new byte[8];
byte[] key = new byte[16];
key[0] = (byte) 150;
key[1] = 56;
key[2] = 47;
key[3] = (byte) 134;
key[4] = (byte) 227;
key[5] = 62;
key[6] = 74;
key[7] = 102;
key[8] = (byte) 178;
key[9] = (byte) 156;
key[10] = 45;
key[11] = 99;
key[12] = (byte) 222;
key[13] = (byte) 212;
key[14] = 84;
key[15] = (byte) 190;
iv[0] = (byte) 238;
iv[1] = 25;
iv[2] = (byte) 170;
iv[3] = 1;
iv[4] = (byte) 227;
iv[5] = 70;
iv[6] = 47;
iv[7] = 70;
KeyGenerator keyGenerator = KeyGenerator.getInstance("Blowfish");
keyGenerator.init(128);
Key secretKey = keyGenerator.generateKey();
Cipher cipherOut = Cipher.getInstance("Blowfish/CFB/NoPadding");
cipherOut.init(Cipher.ENCRYPT_MODE, secretKey);
ArrayList<String> fileNames = new ArrayList<String>();
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64.iso");
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64_2.iso");
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64_3.iso");
fileNames.add("/opt/isos/kubuntu-13.04-desktop-amd64_4.iso");
ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < fileNames.size(); i++) {
System.out.println("Starting encryption: " + i);
CryptoEngine cEnginge = new CryptoEngine(fileNames.get(i), iv, key);
executor.execute(cEnginge);
}
System.out.println("All tasks sended to queue " + System.currentTimeMillis());
executor.shutdown();
while(!executor.isTerminated()){}
final long stopTime = System.currentTimeMillis();
float ttime = ((stopTime - startTime) / 1000);
System.out.println("Total execution time: " + ttime + " seconds");
System.out.println("Total execution time: " + ttime / 60 + " minutes");
}
}
Retestando no VTune
Finalizada a alteração em nosso código Java, podemos gerar o runnable jar file novamente e executar um novo teste no VTune. Novamente, irá levar um bom tempo, porém, espera-se que seja mais rápido, dependendo diretamente de quantos Cores estejam disponíveis em seu sistema e da carga do mesmo.
Ao final da coleta já observamos de cara a diferença de tempos, tendo simplesmente implementado paralelismo em nosso aplicativo:
Se observamos o uso do processador, vemos que saímos de uma situação de baixíssimo uso para uma situação Ideal na maios parte do tempo:
Comparando os resultados
Um recurso muito útil do VTune é a possibilidade de comparação entre os resultados. Na imagem abaixo mostramos onde clicar para efetuar a comparação entre os dois resultados obtidos:
Feito isso, basta selecionar os mesmo e clicar em Compare. Veremos que a diferença de tempo foi na casa de 10 minutos!
Além disso, é possível ver que a execução com paralelismo mante-se sempre perto do ideal do uso do processador:
Conclusões
Em tempos nos quais a concorrência entre as empresas para desenvolver os aplicativos mais eficientes está cada vez mais acirrada, contarmos com uma boa ferramenta de tunning e aplicar as técnicas corretas é muito importante.
O uso do Intel VTune pode trazer benefícios claros para sua empresa no desenvolvimento de aplicativos.
Próximos passos
Os relatórios gerados pelo VTune são muito detalhados, por isso, podem assustar a princípio, mas uma boa leitura da documentação, pode trazer ainda mais benefícios na localização de gargalos no sistema. É indicado tal leitura.
Além disso, nos próximos artigos desta série estaremos abordando o desenvolvimento em outras linguagem e detalharemos a interpretação dos relatórios citados logo acima.
Referências