O real desempenho de um workload pode ser mensurado em ciclos por instrução - Cycles Per Instruction (CPI) – através de contadores de performace. A contagem de eventos do processador permite analisar a performance de aplicações, compiladores e também do próprio hardware. Este artigo apresenta os contadores de performance na plataforma Power7 e suas respectivas funções, e mostra como coletar e analisar essas informações no Linux através de ferramentas de performance como oprofile e perf.
Contadores de Performance em POWER7
A arquitetura do microprocessador POWER7 fornece unidades de monitoramento de performance – Performance Monitor Units (PMUs) - para registro de eventos de performance nos registradores. Em POWER7 existem diversos contadores programáveis disponíveis para contar eventos que podem calcular os componentes de CPI e determinar como melhorar a performance de um workload. Desta forma, você pode monitorar eventos sensíveis de performance específicos quebrando o CPI do seu workload em componentes individuais baseado no que o pipeline está fazendo. Esta abordagem é conhecida como análise detalhada de CPI, ou em inglês, “CPI breakdown analysis”.
CPI – Ciclos Por Instrução - é a métrica ideal para caracterizar a performance de um workload em arquiteturas modernas Multi-Threading. O termo “Ciclos por Instrução” é definido como o número de ciclos de clock do processador necessários para completar uma instrução. CPI é uma quantidade relativa e de forma simplificada é representada pela seguinte fórmula:
CPI = Total de ciclos / instruções completas
Um valor de CPI alto significa baixa utilização dos recursos da máquina. Neste contexto de desempenho o que medimos é o “throughput” da máquina, ou seja, quantas instruções completas o hardware é capaz de entregar em uma fração de tempo. Fica claro que esta métrica de desempenho não se trata de quão rápido o hardware completa uma instrução, mas quantas operações ele é capaz de processar em paralelo e isto implica em quão ocupado está o hardware.
A análise de CPI permite caracterizar workloads, monitorar o pipeline de execução, e identificar problemas de performance nas aplicações, compiladores e no próprio hardware. Este trabalho requer um conhecimento mais profundo dos eventos do pipeline e das ferramentas de performance para processar as informações.
A arquitetura POWER7 permite despachar grupos de instruções a cada ciclo e as instruções são executadas fora de ordem no pipeline de excução. Os estágios do pipeline são:
- BUSCA
As instruções são buscadas do cache de instrução. - DESPACHO
As instruções são colocadas em grupos de até 6 instruções e distribuídas para as filas de emissão. Uma entrada na tabela GCT – Global Completion Table – é criada para rastrear cada grupo que foi despachado e está em execução no pipeline. - EMISSÃO
Até 8 instruções por vez são movidas das filas de emissão para as unidades funcionais de destino. - EXECUÇÃO
Instruções despachadas em ordem são executadas e terminadas fora de ordem em qualquer unidade funcional. - TÉRMINO
Um grupo de instruções é marcado como completado quando todas as suas instruções terminam de executar. O grupo completo é desalocado da tabela GCT.
A análise de CPI é dividida em três grandes classes de ciclos:
- Grupo de instruções completo
- Tabela GCT vazia
- Grupo(s) presente(s) na tabela GCT, mas nenhum completo (completion stall cycles)
A tabela a seguir contém as possíveis categorias de eventos que podem ocorrer em um ciclo de pipeline relevantes à análise de CPI:
Tabela 1: Classificação dos ciclos no pipeline de execução
Os eventos de ciclos paralisados (completion stall cycles) acontecem em qualquer intervalo de tempo em que nenhum grupo de instruções completou a execução. Esses eventos contém o prefixo _STALL na tabela a seguir.
| Evento | Descrição |
| PM_1PLUS_PPC_CMPL | 1 ou mais instruções completadas |
| PM_CMPLU_STALL | Nenhum grupo completo (tabela GCT não vazia) |
| PM_CMPLU_STALL_BRU | A última instrução completada foi na BRU – Branch Unit |
| PM_CMPLU_STALL_DCACHE_MISS | A última instrução completada sofreu uma falha na cache de dados |
| PM_CMPLU_STALL_DFU | A última instrução completada foi na DFU – Decimal Floating Point Unit |
| PM_CMPLU_STALL_DIV | A última instrução completada foi uma divisão de ponto fixo (número fixo de casas decimais) – FXU – Fixed Point Unit |
| PM_CMPLU_STALL_ERAT_MISS | A última instrução completada sofreu uma falha de mapeamento de endereço (ERAT - Effective-to-Real Address Translation) |
| PM_CMPLU_STALL_FXU | A última instrução completada foi na unidade de ponto fixo - FXU |
| PM_CMPLU_STALL_IFU | A última instrução completada foi na unidade de busca – IFU – Instruction Fetch Unit |
| PM_CMPLU_STALL_LSU | A última instrução completada foi na unidade de Load/Store – LSU – Load Store Unit |
| PM_CMPLU_STALL_REJECT | A última instrução completada sofreu uma rejeição de Load/Store |
| PM_CMPLU_STALL_SCALAR | A última instrução completada foi de ponto flutuante escalar |
| PM_CMPLU_STALL_SCALAR_LONG | A última instrução completada foi uma divisão de ponto flutuante ou raiz quadrada |
| PM_CMPLU_STALL_STORE | A última instrução completada foi um Store. |
| PM_CMPLU_STALL_THRD | Conflito de threads. Grupo de instruções pronto pra completar, porém na vez de outro thread. |
| PM_CMPLU_STALL_VECTOR | A última instrução completada foi vetorial. |
| M_CMPLU_STALL_VECTOR_LONG | A última instrução completada foi de vetor de longa latência. |
| PM_GCT_NOSLOT_BR_MPRED | Tabela GCT vazia devido à falha de predição da instrução |
| PM_GCT_NOSLOT_BR_MPRED_IC_MISS | Tabela GCT vazia devido à falha de predição da instrução e falha da cache de instrução |
| PM_GCT_NOSLOT_CYC | A tabela GCT não tem nenhuma entrada deste thread |
| PM_GCT_NOSLOT_IC_MISS | Tabela GCT vazia devido à falha da cache de instrução |
| PM_GRP_CMPL | Execução do grupo de instruções completo |
| PM_RUN_CYC | Ciclos de processamento em que sistema não está ocioso |
| PM_RUN_INST_CMPL | Número de instruções completadas |
Tabela 2: Lista de eventos relevantes à análise de CPI
Ciclos base completados
BASE_COMPLETION_CPI = PM_1PLUS_PPC_CMPL / PM_RUN_INST_CMPL
Ciclos nos quais um grupo completou
COMPLETION_CPI = PM_GRP_CMPL / PM_RUN_INST_CMPL
Ciclos devido ao overhead de expansão
EXPANSION_OVERHEAD_CPI = COMPLETION_CPI - BASE_COMPLETION_CPI
Ciclos paralisados pela Unidade de Ponto-fixo FXU
FXU_STALL_CPI = PM_CMPLU_STALL_FXU/PM_RUN_INST_CMPL
Ciclos paralisados pelas instruções de Ponto-fixo multi-ciclo
FXU_MULTI_CYC_CPI = PM_CMPLU_STALL_DIV/PM_RUN_INST_CMPL
Outros ciclos paralisados pela FXU
FXU_STALL_OTHER_CPI = FXU_STALL_CPI – FXU_MULTI_CYC_CPI
Ciclos com a tabela GCT vazia
GCT_EMPTY_CPI = PM_GCT_NOSLOT_CYC/PM_RUN_INST_CMPL
Ciclos com a tabela GCT vazia devido à falha da cache de instrução
GCT_EMPTY_IC_MISS_CPI = PM_GCT_NOSLOT_IC_MISS/PM_RUN_INST_CMPL
Ciclos com a tabela GCT vazia devido à falha de predição da instrução
GCT_EMPTY_BR_MPRED_CPI = PM_GCT_NOSLOT_BR_MPRED/PM_RUN_INST_CMPL
Ciclos com a tabela GCT vazia devido às falhas de cache e predição de instrução
GCT_EMPTY_BR_MPRED_IC_MISS_CPI = PM_GCT_NOSLOT_BR_MPRED_IC_MISS/PM_RUN_INST_CMPL
Outros ciclos de tabela GCT vazia
GCT_EMPTY_OTHER_CPI = (PM_GCT_NOSLOT_CYC - PM_GCT_NOSLOT_IC_MISS – PM_GCT_NOSLOT_BR_MPRED - PM_GCT_NOSLOT_BR_MPRED_IC_MISS) / PM_RUN_INST_CMPL
Ciclos paralisados pela unidade de busca da instrução IFU
IFU_STALL_CPI = PM_CMPLU_STALL_IFU/PM_RUN_INST_CMPL
Ciclos paralisados pela unidade de execução BRU
IFU_STALL_BRU_CPI = PM_CMPLU_STALL_BRU/PM_RUN_INST_CMPL
Ciclos paralisados por outras operações IFU
IFU_STALL_OTHER_CPI = IFU_STALL_CPI – IFU_STALL_BRU_CPI
Ciclos paralisados pela unidade de Load/Store LSU
LSU_STALL_CPI = PM_CMPLU_STALL_LSU / PM_RUN_INST_CMPL
Ciclos paralisados por rejeições na unidade LSU
LSU_STALL_REJECT_CPI = PM_CMPLU_STALL_REJECT / PM_RUN_INST_CMPL
Ciclos paralisados por traduções ERAT
LSU_STALL_ERAT_MISS_CPI = PM_CMPLU_STALL_ERAT_MISS / PM_RUN_INST_CMPL
Ciclos paralisados por outras rejeições na unidade LSU
LSU_STALL_REJECT_OTHER_CPI = LSU_STALL_REJECT_CPI - LSU_STALL_ERAT_MISS_CPI
Ciclos paralisados devido à falha de perda da cache de dados L1
LSU_STALL_DCACHE_MISS_CPI = PM_CMPLU_STALL_DCACHE_MISS/PM_RUN_INST_CMPL
Ciclos paralisados por outras operações na LSU
LSU_STALL_OTHER_CPI = LSU_STALL_CPI - LSU_STALL_REJECT_CPI - LSU_STALL_DCACHE_MISS_CPI – LSU_STALL_STORE_CPI
Outros ciclos paralisados
OTHER_STALL_CPI = STALL_CPI - FXU_STALL_CPI - VSU_STALL_CPI - LSU_STALL_CPI - IFU_STALL_CPI - SMT_STALL_CPI
Total de ciclos por instrução
RUN_CPI = PM_RUN_CYC / PM_RUN_INST_CMPL
Ciclos paralisados devido ao Multi-Threading simétrico (SMT)
SMT_STALL_CPI = PM_CMPLU_STALL_THRD / PM_RUN_INST_CMPL
Ciclos paralisados
STALL_CPI = PM_CMPLU_STALL / PM_RUN_INST_CMPL
Ciclos paralisados pela unidade vetor e escalar
VSU_STALL_CPI = (PM_CMPLU_STALL_SCALAR + PM_CMPLU_STALL_VECTOR + PM_CMPLU_STALL_DFU) / PM_RUN_INST_CMPL
Ciclos paralisados pela unidade decimal de ponto-flutuante
VSU_STALL_DFU_CPI = PM_CMPLU_STALL_DFU / PM_RUN_INST_CMPL
Ciclos paralisados devido à operações VSU escalar
VSU_STALL_SCALAR_CPI = PM_CMPLU_STALL_SCALAR / PM_RUN_INST_CMPL
Ciclos paralisados devido à operações VSU escalar longo
VSU_STALL_SCALAR_LONG_CPI = PM_CMPLU_STALL_SCALAR_LONG / PM_RUN_INST_CMPL
Ciclos paralisados devido à outras operações VSU escalar
VSU_STALL_SCALAR_OTHER_CPI = VSU_STALL_SCALAR_CPI – VSU_STALL_SCALAR_LONG_CPI
Ciclos paralisados devido à operações VSU de vetor
VSU_STALL_VECTOR_CPI = PM_CMPLU_STALL_VECTOR / PM_RUN_INST_CMPL
Ciclos paralisados devido à operações VSU de vetor longo
VSU_STALL_VECTOR_LONG_CPI = PM_CMPLU_STALL_VECTOR_LONG / PM_RUN_INST_CMPL
Ciclos paralisados devido à outras operações VSU de vetor
VSU_STALL_VECTOR_OTHER_CPI = VSU_STALL_VECTOR_CPI - VSU_STALL_VECTOR_LONG_CPI
A tabela a seguir mostra como essas métricas são representadas na hierarquia de eventos do processador POWER7:
Tabela 3: Representação hierárquica das métricas da análise de CPI
Agora que os eventos e as métricas foram apresentadas podemos focar mais na métrica RUN_CPI. Repare que apenas os ciclos não ociosos são usados para o cálculo de CPI. O total de ciclos por instrução é dado pela divisão dos ciclos não ociosos por instruções completadas, como mostrado a seguir:
RUN_CPI = PM_RUN_CYC / PM_RUN_INST_CMPL |
Ciclos por Instrução é igual ao total de ciclos não ociosos dividido pelo número de instruções completadas.
Coletando informações dos contadores
A técnica de profiling é uma abordagem bastante comum para coletar dados de tempo e utilização de recursos para um workload. Através do profiling é possível identificar problemas nas instruções e nos dados, além de encontrar áreas potenciais para melhoria de performance no código de um programa.
Nesta seção fazemos uma análise de CPI detalhada através de profiling dos eventos de hardware fornecidos pela PMU. Perf e Oprofile são as ferramentas mais populares de performance para profiling e análise de CPI em Linux.
A tabela a seguir contém os eventos de hardware com seus respectivos códigos pelos quais eles são mapeados pela ferramenta perf.
| Evento | Código |
| PM_1PLUS_PPC_CMPL | 0x100f2 |
| PM_CMPLU_STALL | 0x4000a |
| PM_CMPLU_STALL_BRU | 0x4004e |
| PM_CMPLU_STALL_DCACHE_MISS | 0x20016 |
| PM_CMPLU_STALL_DFU | 0x2003c |
| PM_CMPLU_STALL_DIV | 0x40014 |
| PM_CMPLU_STALL_ERAT_MISS | 0x40018 |
| PM_CMPLU_STALL_FXU | 0x20014 |
| PM_CMPLU_STALL_IFU | 0x4004c |
| PM_CMPLU_STALL_LSU | 0x20012 |
| PM_CMPLU_STALL_REJECT | 0x40016 |
| PM_CMPLU_STALL_SCALAR | 0x40012 |
| PM_CMPLU_STALL_SCALAR_LONG | 0x20018 |
| PM_CMPLU_STALL_STORE | 0X2004a |
| PM_CMPLU_STALL_THRD | 0x1001c |
| PM_CMPLU_STALL_VECTOR | 0x2001c |
| M_CMPLU_STALL_VECTOR_LONG | 0x4004a |
| PM_GCT_NOSLOT_BR_MPRED | 0x4001a |
| PM_GCT_NOSLOT_BR_MPRED_IC_MISS | 0x4001c |
| PM_GCT_NOSLOT_CYC | 0x100f8 |
| PM_GCT_NOSLOT_IC_MISS | 0x2001a |
| PM_GRP_CMPL | 0x100f2 |
| PM_RUN_CYC | 0x200f4 |
| PM_RUN_INST_CMPL | 0x400fa |
Tabela 2: Códigos dos eventos de hardware para o perf
A ferramenta perf deve ser executada em um comando ou programa. No entanto, como queremos coletar as estatísticas para a duração de um workload com um conjunto de comandos e programas, não podemos usar o perf em um único programa. Ao invés disso, rodamos o perf em um comando sleep com tempo longo – um dia, usando a opção -a para coletar todas as estatísticas. Quando o workload termina, matamos o processo do comando sleep, o que faz com que o perf retorne as estatísticas e termine. Desta forma os contadores registram os eventos que aconteceram no sistema durante a execução do workload. Esta abordagem pode usada para analisar o que acontece em todo o sistema sem a necessidade do perf saber qual comando ou programa está sendo monitorado.
Importante: O perf abre muitos arquivos para coletar as estatísticas do sistema. Certifique-se de que o limite máximo de arquivos que podem ser abertos comporta a quantidade de eventos monitorados. Consulte os comandos ulimit e lsof para mais informações.
O exemplo a seguir mostra como coletar os contadores referentes à análise de CPI no sistema:
$ perf stat -a --event=r400fa --event=r100f2 --event=r4001a --event=r100f8
--event=r4001c --event=r2001a --event=r200f4 --event=r2004a --event=r4004a
--event=r4004e --event=r4004c --event=r20016 --event=r40018 --event=r20012
--event=r40012 --event=r20018 --event=r4000a --event=r2001c --event=r2003c
--event=r1001c --event=r1003c --event=r20014 --event=r40014 --event=r30004 sleep 1d
Performance counter stats for 'sleep 1d':
20642995717523 raw 0x400fa [17.68%]
6351700835673 raw 0x100f2 [18.91%]
6592401749617 raw 0x4001a [15.03%]
27815093807308 raw 0x100f8 [ 8.08%]
255657047028 raw 0x4001c [ 7.99%]
1717232354921 raw 0x2001a [12.04%]
346189430203434 raw 0x200f4 [16.04%]
3960811777916 raw 0x2004a [ 7.99%]
173599336466 raw 0x4004a [11.98%]
80958163849688 raw 0x4004e [ 3.98%]
81233385317385 raw 0x4004c [ 3.99%]
191318194194427 raw 0x20016 [ 7.98%]
564560376035 raw 0x40018 [ 7.98%]
216813544374568 raw 0x20012 [ 7.98%]
8553663838719 raw 0x40016 [ 7.98%]
16396730779 raw 0x40012 [ 4.12%]
243815645456 raw 0x20018 [ 8.37%]
310390576740069 raw 0x4000a [ 8.39%]
250666680056 raw 0x2001c [ 8.15%]
296680513415 raw 0x2003c [ 4.14%]
109059471221 raw 0x1001c [ 8.35%]
2515554688530 raw 0x1003c [ 4.08%]
13535182790745 raw 0x20014 [ 8.09%]
223953581841 raw 0x40014 [12.10%]
6345222575236 raw 0x30004 [16.11%]
1677.714054029 seconds time elapsed
|
Exemplo 1: Análise de Ciclos por Instrução (CPI) usando perf
Os percentuais da terceira coluna representam a parcela de tempo em que os contadores estão efetivamente registrando os eventos em relação ao tempo em que estão ativos. Se reduzirmos o número de eventos para 2 ou 3 eventos o kernel não terá que rotacionar os eventos através dos contadores disponíveis, dando aos eventos 100% de uso dos contadores.
O exemplo acima aponta para um número de falhas de cache (cache miss) elevado, o que implica muitas vezes em queda de performance da aplicação e do sistema como um todo. Existem diversas técnicas para reduzir falhas de cache como reescrever linhas do código que acessam os dados da memória de uma forma eficiente. O programa a seguir mostra como a simples troca de ordem de loops encadeados pode fazer uma grande diferença na otimização do código.
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
void main(){
int a[1000][1000], i, j;
srand ( time(NULL) );
for (i=0;i<1000;i++){
for (j=0;j<1000;j++){
a[j][i] = rand() % 100;
}
}
for (i=0;i<1000;i++){
for (j=0;j<1000;j++){
a[j][i] = a[j][i]*2;
}
}
return;
}
|
Note que a cada iteração do loop o acesso à memória resulta em falha da cache por causa dos longos saltos dados pelo índice j no loop interno. Esses saltos são ilustrados no exemplo a seguir:
Nessa analogia a memória é acessada horizontalmente e quando j é incrementado, todos os índices i são saltados. Por outro lado, se invertermos os loops, o salto do índice j será de 1 e os dados serão acessados sequencialemente. O código invertido e o respectivo acesso à memória são mostrados a seguir:
for (j=0;j<1000;j++){
for (i=0;i<1000;i++){ |
Para exemplificar na prática as duas situações descritas acima usamos a ferramenta de profiling oprofile para contar os eventos de cache miss enquanto o programa é executado. O programa original é o cache_miss e o modificado com o loop invertido nomeado de cache_hit.
opcontrol --deinit
opcontrol -e PM_DATA_FROM_L2MISS_GRP16:1000
opcontrol --init
opcontrol --reset; opcontrol --start; ./cache_hit ;
opcontrol --stop ; opreport > L2_hit.out
Signalling daemon... done
Profiler running.
Stopping profiling.
opcontrol --reset; opcontrol --start; ./cache_miss ;
opcontrol --stop ; opreport > L2_miss.out
Signalling daemon... done
Profiler running.
Stopping profiling.
cat L2* |grep cache_
1 0.0766 cache_hit
32 2.3616 cache_miss
|
Através desse simples exemplo é fácil identificar a redução de eventos de cache miss no programa modificado cache_hit.
A análise de CPI permite determinar o real uso do hardware para um determinado workload. Essa abordagem vai além de simplesmente medir a performance de aplicações, compiladores, ou do próprio hardware. A métrica “Ciclos Por Instrução” aponta os gargalos na execução das instruções e facilita no desenvolvimento e ajuste de aplicações otimizadas que utilizam o hardware de maneira eficiente.
https://www.power.org/events/Power7/
http://oprofile.sourceforge.net/docs/ppc64-power7-events.php
http://www.ibm.com/developerworks/power/library/pa-cpipower1/
https://www.ibm.com/developerworks/wikis/display/LinuxP/Taking+advantage+of+oprofile

Rafael é engenheiro de software no Linux Technology Center há mais de 6 anos e atua nas áreas de Power, PowerVM, Performance, BigData e HPC. Ele tem 10 anos de experiência em desenvolvimento e qualidade de software e é autor de 4 Redbooks. DevWorks ID: rfolco. Perfil My developerWorks