• Nenhum resultado encontrado

Uma abordagem estática para recomendar estruturas de dados Java para melhorar o consumo de energia

N/A
N/A
Protected

Academic year: 2021

Share "Uma abordagem estática para recomendar estruturas de dados Java para melhorar o consumo de energia"

Copied!
69
0
0

Texto

(1)

José Benito Fernandes de Araújo Neto

UMA ABORDAGEM ESTÁTICA PARA RECOMENDAR

ESTRUTURAS DE DADOS JAVA PARA MELHORAR O CONSUMO

DE ENERGIA

Universidade Federal de Pernambuco posgraduacao@cin.ufpe.br <www.cin.ufpe.br/~posgraduacao>

RECIFE 2016

(2)

José Benito Fernandes de Araújo Neto

UMA ABORDAGEM ESTÁTICA PARA RECOMENDAR

ESTRUTURAS DE DADOS JAVA PARA MELHORAR O CONSUMO

DE ENERGIA

Trabalho apresentado ao Programa de Pós-graduação em Ciência da Computação do Centro de Informática da Univer-sidade Federal de Pernambuco como requisito parcial para obtenção do grau de Mestre em Ciência da Computação.

Orientador: Fernando José Castor de Lima Filho Co-Orientador: Gustavo Henrique Lima Pinto

RECIFE 2016

(3)

Catalogação na fonte

Bibliotecária Monick Raquel Silvestre da S. Portes, CRB4-1217

A662a Araújo Neto, José Benito Fernandes de

Uma abordagem estática para recomendar estruturas de dados Java para melhorar o consumo de energia / José Benito Fernandes de Araújo Neto. – 2016.

68 f.: il., fig., tab.

Orientador: Fernando José Castor de Lima Filho.

Dissertação (Mestrado) – Universidade Federal de Pernambuco. CIn, Ciência da Computação, Recife, 2016.

Inclui referências e apêndice.

1. Engenharia de software. 2. Consumo de energia. I. Lima Filho, Fernando José Castor de (orientador). II. Título.

005.1 CDD (23. ed.) UFPE- MEI 2017-22

(4)

José Benito Fernandes de Araújo Neto

Uma Abordagem Estática para Recomendar Estruturas de Dados

Java Para Melhorar o Consumo de Energia

Dissertação de Mestrado apresentada ao Programa de Pós-Graduação em Ciência da Computação da Universidade Federal de Pernambuco, como requisito parcial para a obtenção do título de Mestre em Ciência da Computação

Aprovado em: 30/08/2016.

BANCA EXAMINADORA

__________________________________________ Prof. Dr. Kiev dos Santos Gama

Centro de Informática / UFPE

__________________________________________ Prof. Dr. Gustavo Henrique Porto de Carvalho

Escola Politécnica de Pernambuco / UPE

__________________________________________ Prof. Dr. Fernando José Castor de Lima Filho

Centro de Informática / UFPE

(5)

Eu dedico esta dissertação a toda minha família, amigos e professores que me deram o apoio necessário para torná-la realidade.

(6)

Agradecimentos

Aos meus pais que sem eles não chegaria onde estou. A toda minha família que me apoio e me incentivou.

A minha esposa Elys Fragoso, agradeço por todos esses anos de companheirismo e pela confiança que sempre teve em mim.

Ao meu Orientador, Fernando Castor, e Co-orientador, Gustavo Pinto, que acreditaram em mim e me deram o apoio necessário para seguir essa jornada.

A todos os meus amigos e a todas as pessoas que acreditaram em mim e me deram oportunidades tanto academicamente quanto profissionalmente.

(7)

No meio de qualquer dificuldade encontra-se a oportunidade. —ALBERT EINSTEN

(8)

Resumo

Programadores Java possuem um amplo repertório de coleções a seu dispor. Essas coleções implementam abstrações bem conhecidas como listas, conjuntos, filas, e tabelas. Além disso, a linguagem conta com um rico acervo de coleções que podem ser utilizadas paralelamente por múltiplas threads em execução, sem comprometer a corretude do programa. Estudos anteriores demonstraram que essas coleções possuem diferentes características em termos de desempenho, escalabilidade, controle de concorrência e consumo de energia. Em particular, Pinto et al. (2016) investigaram os impactos do uso de diferentes coleções seguras para múltiplas threads no consumo de energia de uma aplicação. Esse estudo descobriu que diferentes operações de uma mesma implementação possuem diferentes características de consumo de energia e o mesmo se aplica a diferentes implementações de uma mesma abstração. Tendo em vista esta diferença do consumo de energia de acordo com a coleção e operação utilizadas, este trabalho tem por objetivo melhorar a eficiência de energia de aplicações concorrentes que fazem uso intenso de coleções através da recomendação automática de estruturas de dados mais eficientes em um determinado contexto. Dessa forma, este trabalho desenvolveu uma abordagem baseada em análise estática para analisar de forma automática o uso das estruturas de dados de uma aplicação e recomendar transformações necessárias de acordo com suas utilizações, de modo a reduzir o consumo de energia da aplicação. Mais especificamente analisou-se um total de 11 implementações de três tipos de estrutura de dados seguras para threads disponíveis na linguagem Java: conjuntos (4), mapas (4) e sequências (3). Utilizando a biblioteca WALA, a implementação da abordagem proposta apresenta recomendações das transformações necessárias para que as aplicações consumam menos energia. Esta recomendação é realizada através de uma heurística baseada no consumo de energia de cada coleção para um determinado ambiente. Para avaliar a abordagem, foram utilizados benchmarks, conhecidos na academia, de aplicações reais. Utilizando a abordagem proposta nesse trabalho foi percebida uma redução do consumo de energia de até 4.37%. Até onde foi possível averiguar, este é o primeiro trabalho que torna possível reduzir o consumo de energia de uma aplicação sem a necessidade de executá-la.

(9)

Abstract

Java developers have a wide repertoire of collections at their disposal. These collections implement well-known abstractions such as lists, sets, queues, and maps. In addition, the Java language has a rich framework of collections that can be used in parallel by multiple threads running without compromising the correctness of the program. Previous work has shown that these collections have different characteristics in terms of performance, scalability, concurrency control, and energy consumption. In particular, Pinto et al. (2016) investigated the impact of using different thread safe collections on the energy consumption of an application. Among the findings, this study found that different operations of the same implementation have different characteristics, in terms of energy consumption, and the same applies to different implementations of the same abstraction. Since both dimensions, the operations and the collection implementation, impact energy, this work aims to improve the energy efficiency of concurrent applications that do intense use of collections by automatically recommending potentially more efficient collection implementations in a given context. Our approach leverages static analysis to collect information about how applications use thread-safe collections from Java and, based on the energy usage profiles of the operations of these collections, make informed recommendations that potentially save energy. We have developed CECOTOOL, a tool that implements this approach. Usage of the tool does not require the application under analysis to be executed. To evaluate the approach, we used well-known benchmarks based on two real-world applications. Using this approach we observed a reduction in energy consumption of up to 4.37%. To the best of our knowledge, this is the first work aiming to reduce the energy consumption of an application without the need to run it.

(10)

Lista de Figuras

2.1 Código exemplo de utilização do jRAPL . . . 20

2.2 Exemplo de reaching definitions: definições em 1 e 5 alcançam o uso de y em 4 . . 22

2.3 Exemplo de verificação segura utilizando análise inter-procedural . . . 22

2.4 Exemplo de código para análise inter-procedural context sensitivity . . . 23

2.5 Exemplo de código no qual a análise 0-CFA consegue distinguir as chamadas do método get . . . 24

2.6 Código exemplo de utilização WALA . . . 26

3.1 Fluxo da abordagem . . . 28

3.2 Código exemplo de utilização de uma coleção do tipo Vector . . . 30

3.3 Exemplo de utilização do loop foreach . . . 33

3.4 Exemplo de nível aninhamento de loops . . . 33

4.1 Xalan - exemplo de transformação de código . . . 41

4.2 Fluxo de execução do experimento . . . 42

4.3 Perfis de consumo de energia para as implementações de List . . . 45

4.4 Perfis de consumo de energia para as implementações de Map . . . 45

4.5 Xalan - exemplo onde o tipo de uma variável coleção não foi modificado (para CopyOnWriteArrayList), apesar da recomendação . . . 49

4.6 Tomcat - exemplo de não transformação para ConcurrentHashMap . . . 51

(11)

Lista de Tabelas

3.1 Coleções, implementações e métodos . . . 28

3.2 Exemplo de contexto aplicado à operação elementAt . . . 32

3.3 Exemplo de contexto aplicado à operação elementAt, dentro de dois loops aninhados 32 3.4 Valores para a variável coleção imports quando seu tipo for Vector . . . 35

3.5 Valores para a variável coleção imports quando seu tipo for synchronizedList . . . 35

3.6 Exemplo de saída da análise do uso das estrutura de dados . . . 37

3.7 Exemplo da saída da recomendação . . . 38

4.1 Recomendações Xalan × Quantidade na versão original . . . 47

4.2 Recomendações Tomcat × Quantidade na versão original . . . 47

4.3 Xalan: transformações realizadas . . . 49

4.4 Equivalência entres os métodos de Vector e List . . . 50

4.5 Tomcat: transformações realizadas . . . 50

4.6 Xalan: resultados das 600 execuções . . . 52

4.7 Tomcat: resultados das 600 execuções . . . 52

4.8 Gráficos de Box Plot para as 600 execuções . . . 52

4.9 Xalan: consumo de energia para o Ambiente2 . . . 53

4.10 Tomcat: consumo de energia para o Ambiente2 . . . 53

4.11 Sumarização das transformações realizadas . . . 54

4.12 Percentual das transformações realizadas . . . 55

4.13 Situações que não envolveu apenas a troca do tipo da variável ou em que a transfor-mação não foi realizada . . . 55

(12)

Lista de Acrônimos

JCF Java Collections Framework . . . 17

RAPL Running Average Power Limit. . . 19

MSR Machine-specific register . . . 20

SO Sistema Operacional . . . 20

CFG Control Flow Graph. . . 21

CFA Control Flow Analysis. . . 23

RTA Rapid Type Analysis. . . 23

IR Representação Intermediária . . . 25

SSA form Static Single Assignment form. . . 25

EPB Energy Profile Benchmarks. . . 29

JVM Máquina Virtual Java . . . 29

(13)

Sumário 1 Introdução 14 1.1 Objetivo . . . 15 1.2 Contribuições . . . 16 1.3 Organização da Dissertação . . . 16 2 Fundamentação 17 2.1 Coleções e Coleções Seguras para Threads em Java . . . 17

2.1.1 List . . . 17

2.1.2 Map . . . 18

2.1.3 Set . . . 18

2.2 Métodos para Medição de Consumo de Energia em Aplicações de Software . . . . 19

2.2.1 RAPL . . . 20 2.2.2 jRAPL . . . 20 2.3 Análise Estática . . . 21 2.3.1 Conceitos Gerais . . . 21 2.3.2 WALA . . . 24 3 Abordagem 27 3.1 Visão Geral da Abordagem . . . 27

3.2 Perfis de Consumo de Energia das Coleções . . . 28

3.3 Análise Estática das Aplicações . . . 29

3.3.1 Estimativa da Frequência de Uso . . . 31

3.3.2 Análise de Loops . . . 31

3.4 Recomendação . . . 33

3.5 Implementação . . . 36

3.5.1 CECOTOOL- Análise de Uso das Coleções . . . 36

3.5.2 CECOTOOL- Recomendação . . . 37

4 Avaliação 39 4.1 Metodologia . . . 39

4.1.1 Benchmarks . . . 39

4.1.2 Transformações nos Benchmarks . . . 40

4.1.3 Medição de Consumo de Energia . . . 40

4.1.4 Configuração dos Ambientes . . . 41

4.1.5 Execução dos Experimentos . . . 42

4.1.6 Execução dos Energy Profile Benchmarks (EPB) . . . 43

(14)

4.2 Resultados dos Experimentos . . . 44

4.2.1 Etapa 1 - Análise dos Perfis de Consumo de Energia . . . 44

4.2.2 Etapa 2 - Execução da CECOTOOL . . . 46

4.2.3 Etapa 3 - Realização das Transformações Sugeridas . . . 48

4.2.4 Etapa 4 - Execução e Medição das versões (Original e Transformada) nos Ambientes 51 4.3 Análise dos Resultados . . . 53

4.4 Ameaças à Validade . . . 55

5 Trabalhos Relacionados 57 5.1 Métodos para Predizer o Consumo de Energia . . . 57

5.2 Modificações de Código que Visam Melhorar o Consumo de Energia . . . 58

5.3 Estudos Sobre Coleções . . . 58

6 Conclusão 61 6.1 Trabalhos Futuros . . . 61

Referências 63

Apêndice 67

(15)

14 14 14

1

Introdução

Hoje em dia, sistemas de software não são somente utilizados em computadores pessoais, mas também em dispositivos móveis, dispositivos utilizados junto ao corpo (por exemplo, Apple Watchou Google Glass), em carros inteligentes e em veículos aéreos não tripulados. Em muitos dos contextos em que um sistema de software é desenvolvido, um problema crítico porém comumente negligenciado é o consumo de energia desses sistemas. Embora sistemas de software não consumam energia diretamente, eles afetam a utilização do hardware, conduzindo a um indireto consumo de energia.

Esta preocupação, se não abordada propriamente, pode não somente impactar negativa-mente na receita/faturamento destes software, mas pode também emitir centenas de toneladas de dióxido de carbono na atmosfera. Como consequência de anos de esforços para melhorar a produtividade das etapas de desenvolvimento do software, ao invés de minimizar a utilização de recursos, em 2012 o consumo de energia de sistemas de software estava estimado 5% do total de consumo de energia utilizado no globo (GELENBE; CASEAU, 2015). Com novos sistemas de software sendo implantados todos os dias, é esperado que nos próximos anos este consumo aumente.

Por muitos anos, a pesquisa que conecta sistemas de software com consumo de energia se concentrou nas camadas mais baixas do hardware e da pilha de software, como compiladores, máquinas virtuais, e sistemas de runtime. Tais soluções energeticamente eficientes são, hoje em dia, áreas estabilizadas de pesquisa. No entanto, estudos recentes apontam que estas soluções de baixo nível não capturam todos os possíveis cenários de otimização energética, e otimizações de alto nível podem ser utilizadas como métodos complementares para melhorar o consumo de energia de sistemas de software (GUTIÉRREZ; POLLOCK; CLAUSE, 2014; LI; TRAN; HALFOND, 2014; LI et al., 2013).

Em particular, essas soluções contam com o conhecimento do programador, que detém informações sobre o contexto em que a aplicação é executada — informação esta desconhecida pelos sistemas de baixo nível. O consumo de energia é resultado do software em execução em um hardware em que o contexto (por exemplo, o tipo da computação) é de crucial importância. Estudos recentes têm focado no consumo de energia no nível da aplicação de software nos mais diversos contextos, como por exemplo, em técnicas de programação paralela (PINTO; CASTOR;

(16)

1.1. OBJETIVO 15 LIU, 2014b), chamadas de sistema (AGGARWAL et al., 2014) ou em técnicas de refatoração de software (SAHIN; POLLOCK; CLAUSE, 2014).

Dessa forma, alguns pesquisadores focaram seus esforços para melhor entender o con-sumo de energia de estruturas de dados, um dos pilares da programação de computadores. Estes trabalhos apresentaram uma compreensível visão geral sobre o consumo de energia de estruturas de dados não seguras a threads (HASAN et al., 2016), estruturas de dados de uma linguagem funcional (LIMA et al., 2016), e até um framework de suporte a decisão para escolher estruturas de dados mais adequadas (GUTIÉRREZ; POLLOCK; CLAUSE, 2014). Ainda, Pinto et al. (2016) mostraram que diferentes implementações de coleções Java seguras para threads impactam diferentemente no consumo de energia das aplicações, e que diferentes operações de uma mesma implementação de coleção também consomem energia de forma diferente.

O interesse em técnicas que possam minimizar o consumo de energia, levando em conta um cenário de múltiplas threads, tem ganhado notoriedade nos últimos anos (PINTO et al., 2016; PINTO; CASTOR; LIU, 2014b). O uso intenso de acesso e manipulação de dados, frenquente-mente através de coleções, em um ambiente com mais de uma thread, é uma direção crítica que merece ser mais investigada. Isso é importante não apenas para servidores e computadores de mesa, mas também para smartphones e tablets, que utilizam fortemente arquiteturas com mais de um processador e, dessa forma, necessitam de programas concorrentes para melhor utilizar estes recursos. Ademais, atualmente, as principais linguagens de programação fornecem várias implementações para as mesmas coleções e elas têm potencialmente diferentes características com relação ao consumo de energia.

No entanto, considerando o levantamento bibliográfico realizado neste trabalho, nenhum trabalho focado em estruturas de dados, propõe uma abordagem para melhorar o consumo de energia utilizando técnicas de análise estática. A utilização de técnicas de análise estática para melhorar o consumo de energia de um sistema de software é uma direção emergente que tem sido pouco explorada por estudo recentes, uma vez que exime o programador de executar o sistema de software, cujo consumo de energia deseja-se melhorar. No entanto, vários desafios relacionados à precisão são encontrados na atividade de análise estática, por exemplo, capturar o contexto de uso de uma variável de uma coleção.

Tendo em vista este cenário, este trabalho apresenta uma abordagem estática com o objetivo de melhorar o consumo de energia de coleções Java. Diferente de outros trabalhos encontrados na literatura, o presente estudo foca em implementações de coleções seguras para threads, além de realizar experimentos em ambientes de múltiplos processadores.

1.1 Objetivo

Este trabalho tem como objetivo propor uma abordagem que utiliza técnicas de análise estática para propor recomendações de implementações de coleções mais eficientes em um sistema de software, dado seu contexto de execução. Mais especificamente, este trabalho

(17)

1.2. CONTRIBUIÇÕES 16 pretende responder às seguintes questões de pesquisa (QP):

 QP1. É possível melhorar o consumo de energia de uma aplicação através de uma

abordagem estática para recomendação de estruturas de dados?

 QP2. O quão simples é trocar implementações de estruturas de dados com o objetivo

de reduzir o consumo de energia?

1.2 Contribuições

Para ajudar a responder essas perguntas, foi criada a CECOTOOL, que implementa a abordagem proposta neste trabalho. Através da utilização da CECOTOOL em benchmarks não-triviais, foi observado que a técnica proposta é capaz de melhorar o consumo de energia dos benchmarks utilizados em até 4.37% (QP1). Além disso, a maioria das transformações recomendadas pela CECOTOOLsão bastante simples, sendo realizadas, em boa parte dos casos, apenas através de trocas de tipos de implementação de coleção. Isso sugere que a troca de implementações de estruturas de dados com o objetivo de reduzir o consumo de energia é relativamente simples (QP2).

1.3 Organização da Dissertação

Esta dissertação está organizada como segue:

 Capítulo 2 realiza a fundamentação teórica com o intuito de fornecer o conhecimento

base para o acompanhamento da dissertação;

 Capítulo 3 descreve a metodologia e a abordagem proposta por este trabalho;

 Capítulo 4 apresenta os resultados obtidos com a utilização da proposta e a avaliação

desses resultados;

 Capítulo 5 discute os trabalhos relacionados;

 Capítulo 6 resume as principais contribuições científicas deste trabalho e possíveis

(18)

17 17 17

2

Fundamentação

Este capítulo tem como objetivo fundamentar o conhecimento base, necessário para o melhor acompanhamento da dissertação. O Capítulo conta com uma seção para discussão sobre as coleções seguras para threads em Java (Seção 2.1), uma seção sobre o consumo de energia em aplicações (Seção 2.2) e, finalmente, uma seção sobre análise estática e a biblioteca WALA (Seção 2.3).

2.1 Coleções e Coleções Seguras para Threads em Java

Coleções são usadas para armazenar, recuperar e manipular dados agregados. Tipica-mente, elas representam dados que formam um grupo natural, por exemplo uma coleção de alunos. A linguagem de programação Java disponibiliza por padrão o Java Collections Fra-mework(JCF), uma série de interfaces com diferentes implementações utilizadas para representar e manipular diferentes tipos de coleções. Além disso, o JCF implementa vários algoritmos, como por exemplo algoritmos de busca e ordenação, nas classes que implementam estas interfaces.

Além das implementações disponíveis no JCF, a versão 5 do Java adicionou o pacote java.util.concurrent, que conta com várias implementações de coleções seguras para threads, considerando as mesmas interfaces praticadas no JCF. As coleções utilizadas nesse estudo consistem de 11 implementações seguras para threads Java, agrupadas logicamente pelas interfaces List, Map e Set.

Neste trabalho, o termo coleção refere-se ao tipo da estrutura de dados que agrupa os itens: listas, mapas e conjuntos; enquanto o termo implementação de coleções é referente às implementações de fato, por exemplo a classe Vector. A seguir são detalhadas as interfaces, bem como algumas das implementações utilizadas.

2.1.1 List

java.util.List: Listas são coleções ordenadas que permitem elementos duplicados. Através desta coleção, desenvolvedores podem controlar com precisão o local em que um elemento é inserido. Os elementos das listas podem ser acessados através de índices, ou através de um Iterator, percorrendo a lista. Uma vez que o foco deste trabalho está nas

(19)

2.1. COLEÇÕES E COLEÇÕES SEGURAS PARA THREADS EM JAVA 18 coleções seguras a threads, as implementações aqui analisadas são: Vector, Collections. synchronizedList()(para efeitos de simplificação, esta implementação será referenciada como synchronizedList ao longo deste trabalho), e CopyOnWriteArrayList. A principal diferença, em termos de sincronização, entre Vector e synchronizedList é que, apesar de ambas serem seguras para threads nas operações de inserção e remoção, apenas Vector é segura para threads durante operações de leitura. Assim é necessário realizar a sincronização quando se itera sobre synchronizedList. CopyOnWriteArrayList, por outro lado, é uma variação segura para threads o qual todas operações que alterem a lista são realizadas criando uma cópia dela. Dessa forma, CopyOnWriteArrayList é conhecidamente custosa em operações como escrita e remoção. No entanto, no que se refere a leituras, esta coleção é tão eficiente quanto as demais alternativas não seguras a threads (PINTO et al., 2016).

2.1.2 Map

java.util.Map: Mapas são objetos que mapeiam chaves a valores. Um mapa não contém chaves duplicadas. Assim, cada chave pode estar associada a no máximo um valor. Uma inserção de um par (chave, valor), na qual a chave já esteja associada a outro valor, re-sultará na atualização do valor para aquela chave. As implementações seguras para threads escolhidas foram: Hashtable, Collections.synchronizedMap() (para efeitos de simplificação, esta implementação será referenciada como synchronizedMap),

Concur-rentSkipListMape ConcurrentHashMap.

Uma das principais diferenças entre Hashtable e ConcurrentHashMap é que, para operações de inserção, ConcurrentHashMap bloqueia apenas uma porção do mapa enquanto Hashtable bloqueia toda a estrutura. Dessa forma, outras threads conseguem acessar outras partes do mapa para operações leitura sem ter que esperar pela thread efetuar o desbloqueio. Ademais, o desempenho de Hashtable degrada à medida que o tamanho do mapa cresce, pois este acaba ficando bloqueado por mais tempo do que necessário. A estrutura synchronizedMap, por sua vez, também realiza o bloqueio da estrutura inteira para cada operação de leitura e escrita. Outra diferença é a ordem em que os itens de um mapa são acessados. Enquanto ConcurrentSkipListMap itera ordenadamente de acordo com a ordem natural das chaves ou conforme criação do mapa, ConcurrentHashMap não garante que ordem da leitura será preservada. Ademais, Collections.synchronizedMap() preserva a ordem do mapa que ele envolve.

2.1.3 Set

java.util.Set: Conjuntos são coleções que não contém itens duplicados. Repre-sentam a abstração matemática de conjuntos. Outra característica é que eles não garantem a ordem na qual os itens serão lidos. Diferente de listas, seus itens não podem ser

(20)

aces-2.2. MÉTODOS PARA MEDIÇÃO DE CONSUMO DE ENERGIA EM APLICAÇÕES DE

SOFTWARE 19

sados através de seus índices. No entanto, similar a listas, operações de leitura podem ser realizadas através de Iterator. As implementações seguras para threads selecionadas para este estudo foram: Collections.synchronizedSet(), ConcurrentSkipListSet,

ConcurrentHashSetand CopyOnWriteArraySet. Note que ConcurrentHashSet

é o retorno do método Collections.newSetFromMap(new ConcurrentHashMap <String,String>()).

As implementações de conjuntos seguros a threads também possuem diferenças com relação aos seus comportamentos e funcionalidades. Por exemplo, CopyOnWriteArraySet possui comportamento semelhante a CopyOnWriteArrayList, sendo bastante custosa em operações como escrita e remoção. Ainda, a implementação synchronizedSet, assim como synchronizedList, também precisa de sincronização externa quando se usa Iterator. Além disso, a implementação ConcurrentSkipListSet, é a versão concorrente de SortedSet. Da mesma forma que ConcurrentSkipListMap, seus os elementos são ordenados de acordo com a ordem natural ou conforme criação do conjunto.

2.2 Métodos para Medição de Consumo de Energia em Aplicações de Software

Um obstáculo que desenvolvedores de software encontram na tentativa de reduzir o consumo de energia de suas aplicações é a falta de informações sobre como as decisões tomadas, durante a construção dessas aplicações, impactam no consumo de energia. Em particular, desen-volvedores atualmente não entendem como as escolhas e compensações que fazem impactam o consumo de energia de seus softwares (PINTO; CASTOR; LIU, 2014a).

No entanto, para cumprir esse objetivo é necessário medir com precisão o consumo de energia de um determinado sistema de software. Nesse contexto, medição de potência e estimativa de energia são amplas áreas de pesquisa que abrangem vários campos, incluindo arquitetura, sistemas operacionais, e engenharia de software. Com relação a métodos para medição de potência, comumente faz-se o uso de hardware para obter amostras de potência. Então, utiliza-se de técnicas de software para atribuir a potência às estruturas de implementação. Uma característica importante dos medidores é a taxa de amostragem: número de amostras por segundo. Essas amostras em geral são medidas em watts (Potência – P). A energia, em joules, é derivada de E = P × tempo.

Por outro lado, a estimativa de energia assume que o desenvolvedor não tem acesso a hardware para medição de potência e usa de técnicas baseadas em software para estimar o consumo de energia de uma aplicação durante sua execução. Um exemplo dessa abordagem é o Running Average Power Limit (RAPL) (DAVID et al., 2010), detalhado no próximo tópico. Seguindo essa abordagem, este trabalho utiliza o jRAPL (LIU; PINTO; LIU, 2015), que fornece um conjunto de APIs de alto nível para realizar a medição do consumo de energia dos benchmarks em Java.

(21)

2.2. MÉTODOS PARA MEDIÇÃO DE CONSUMO DE ENERGIA EM APLICAÇÕES DE

SOFTWARE 20

2.2.1 RAPL

O RAPL (DAVID et al., 2010) é um conjunto de interfaces que permitem monitorar, controlar e receber notificações sobre consumo de energia em diferentes níveis de hardware. Projetada pela Intel para permitir o gerenciamento de energia de seus processadores, RAPL monitora o consumo de energia e armazenam as informações nos registradores Machine-specific registers (MSRs). Esses registradores podem ser acessados pelo Sistema Operacional (SO), através do módulo msr no Linux. A interface RAPL discrimina o consumo de energia nos seguintes níveis:

 Package: Total de energia consumida por todo socket;

 PP0: Total de energia consumida pelos cores;

 PP1: Total de energia consumida pelo uncore (L3 cache, GPUs, conectores);

 DRAM: Total de energia consumida pela memória RAM;

2.2.2 jRAPL

A biblioteca jRAPL (LIU; PINTO; LIU, 2015), por sua vez, fornece um conjunto de APIs de alto nível, escrito em Java, para acessar o módulo RAPL. A interface RAPL promove um amplo suporte para o gerenciamento de consumo de energia, enquanto a biblioteca jRAPL apenas usa as funcionalidades do RAPL para coletar algumas destas informações. Como o módulo msr roda em modo privilegiado no núcleo do Linux, jRPAL funciona de forma semelhante, fazendo uso de chamadas de sistemas para acessar este módulo. A coleta da informação sobre o consumo de energia utilizando jRAPL é simples, basta cercar o bloco de código o qual se deseja medir o consumo, por um par de chamadas ao método statCheck fornecido pela biblioteca. A Figura 2.1 apresenta um exemplo no qual se deseja medir o consumo do método getda variável vector. O valor do consumo de energia, então, é a diferença entre as variáveis

energiaFinale energiaInicial.

1 double energiaInicial = EnergyCheck.statCheck();

2 vector.get(i);

3 double energiaFinal = EnergyCheck.statCheck();

Figura 2.1: Código exemplo de utilização do jRAPL

Caso o processador contenha múltiplas unidades de processamento, jRAPL pode reportar os dados do consumo de energia de forma individual (por processador) ou combinado (somando todos os processadores). Neste trabalho, as informações referentes ao consumo de energia são apresentadas de forma combinada. Uma vantagem de usar o jRAPL é que ele permite medir o

(22)

2.3. ANÁLISE ESTÁTICA 21 consumo de energia considerando diferentes granularidades de código (por exemplo, chamada de métodos ou linhas de códigos).

2.3 Análise Estática

Esta seção visa explanar sobre alguns conceitos da análise estática que foram importantes para o desenvolvimento deste trabalho e sobre a biblioteca WALA (WALA - T.J. Watson Libraries for Analysis, 2016) utilizada na implementação da análise do uso das coleções Java consideradas neste trabalho.

2.3.1 Conceitos Gerais

Técnicas de análise estática oferecem, em tempo de compilação, aproximações seguras para um conjunto de valores ou comportamentos que seriam alcançados dinamicamente, durante a execução do programa (NIELSON; NIELSON; HANKIN, 1999). Como resultado, análises estáticas não necessitam executar a aplicação alvo. Alguns dos usos de análise estática permitem a verificação de diferentes propriedades da aplicação como corretude, robutez e segurança. Téc-nicas de análise estática são primariamente divididas em intra-procedurais ou inter-procedurais, dependendo da forma em que a informação é propagada. A análise intra-procedural propaga informações utilizando o grafo de fluxo de controle (Control Flow Graph (CFG)) específico de um método ou função, enquanto a análise inter-procedural propaga informações através do fluxo de chamadas entre métodos e funções do sistema (Inter-procedural Flow Graph, por exemplo Call Graph).

Dentre as principais técnicas de análise estática de programas destaca-se a Data Flow Analysis. A Data Flow Analysis é uma técnica utilizada para coletar informações sobre conjuntos de valores em vários pontos de um programa. Ela utiliza grafos de fluxo de controle para coletar informações sobre o que pode acontecer em tempo de execução (APPEL; PALSBERG, 2002). Essa técnica é frequentemente utilizada por compiladores para otimizar o código. Um exemplo clássico da análise de dataflow é a análise intra-procedural reaching definitions, a qual determina quais definições podem alcançar um determinado ponto do código. Reaching definitions, por exemplo, pode ser usado para identificar declarações de variáveis que não são utilizadas, ou seja, se uma definição do programa não alcança um uso (NIELSON; NIELSON; HANKIN, 1999). A Figura 2.2 apresenta um exemplo de reaching definitions em que definições nas linhas 1 e 5 alcançam o uso da variável y na linha 4.

Por sua vez, a análise inter-procedural coleta informações através do fluxo de chamada dos métodos (tipicamente através de todo o programa) (NIELSON; NIELSON; HANKIN, 1999). Assim, essas informações fluem entre os métodos. Tais informações podem ser utilizadas para melhorar a análise intra-procedural. Como mencionado, a análise inter-procedural comumente utiliza o inter-procedural flow graph, um grafo de fluxo para todo o programa, ao invés de apenas de um único método, o qual captura o que um método pode realmente fazer durante o programa.

(23)

2.3. ANÁLISE ESTÁTICA 22 1 y = x; 2 z = 1; 3 while( y != 0 ) { 4 z = z * y; 5 y = y - 1; 6 }

Figura 2.2: Exemplo de reaching definitions: definições em 1 e 5 alcançam o uso de y em 4

Por exemplo, a Figura 2.3 mostra um trecho de código onde, através da análise inter-procedural, é possível verificar que este programa é seguro a uma possível divisão por zero.

1 private static int dobro(int i){

2 int y= 2*i;

3 return y;

4 }

5 public static void main(String[] args) {

6 int z = 5; 7 int w = dobro(z); 8 z = 10/w; 9 z = 0; 10 w = dobro(z); 11 }

Figura 2.3: Exemplo de verificação segura utilizando análise inter-procedural

A análise inter-procedural sensível ao contexto (context sensitive analysis) analisa os métodos várias vezes, considerando cada contexto em que este é chamado (ALDRICH, 2013a). Ou seja, re-analiza o método chamado para cada chamada. Entretanto, quando a análise não é sensível a contexto (context insensitive) realiza apensa uma análise independente das chamadas. A Figura 2.4 mostra um exemplo no qual um método é chamado duas vezes em pontos diferentes do programa e passando argumentos diferentes. Na análise sensível a contexto, o resultado é computado para cada chamada, ou seja, será retornado 1 para o primeira chamada do método get(1)(linha 5) e para a segunda chamada (linha 6) será retornado 2. Enquanto, a análise context-insensitivecalcularia apenas um resultado para as duas chamadas: x não é constante. Ainda, pode ocorrer caminhos que não são possíveis de serem percorridos, no exemplo, a chamada get(1) retornar 2 devido a fusão das informações vindas das duas chamadas.

Por outro lado, a análise inter-procedural flow-insensitive , é uma análise que, ao contrário da análise dataflow, não leva em consideração a ordem das declarações dos programas. Análises contexte flow-insensitivity são usadas para melhorar o desempenho da análise inter-procedural. Por exemplo, Andersen’s Points-To Analysis (MØLLER; SCHWARTZBACH, 2015) é uma

(24)

2.3. ANÁLISE ESTÁTICA 23

1 private static int get(int x){

2 return x;

3 }

4 public static void main(String[] args) {

5 int x = get(1); 6 int y = get(2); 7 }

Figura 2.4: Exemplo de código para análise inter-procedural context sensitivity

análise de ponteiros points-to, a qual computa a relação de points-to(p,x), onde p pode ou deve apontar para a localização da variável x. Andersen’s Points-To Analysis é uma análise inter-procedural flow-insensitive e context-insensitive. Para detalhes, o Algoritmo de Andersen’s Points-To Analysisé descrito em (MØLLER; SCHWARTZBACH, 2015).

A análise de programas orientados a objetos é desafiadora no sentido em que não é óbvio qual método é chamado a partir do local da chamada call site. Com o objetivo de construir um call graphpreciso, a análise deve determinar qual tipo do objeto é chamado em cada call site. A abordagem mais simples é a análise de hierarquia de classes (Class Hierarchy Analysis). Esta análise usa o tipo da variável, junto com a hierarquia de classes, para determinar quais tipos de objetos a variável pode apontar. Esta análise é muito imprecisa, apesar de poder ser eficiente, O(n × t). Isto se deve ao fato de que esta análise visita n call sites, e para cada call sites percorre uma sub-árvore de tamanho t da hierarquia da classe (ALDRICH, 2013b).

Uma melhoria para a análise de hierarquia de classes é a Rapid Type Analysis (RTA) (AL-DRICH, 2013b), a qual elimina da hierarquia o que nunca é instanciado. A análise constrói iterativamente um conjunto dos tipos instanciados, nomes dos métodos invocados e os méto-dos concretos chamaméto-dos. RTA pode ser consideravelmente mais precisa do que a análise de hierarquia de classes em programas que usam bibliotecas que definem muitos tipos, e apenas alguns deles são usados no programa. Ela é extremamente eficiente porque precisa percorrer o programa apenas uma vez (O(n)) e então construir o call graph visitando cada um dos n call sitese considerando uma sub-árvore de tamanho t da hierarquia de classes, para um total de O(n × t) vezes.

Call graphsorientado a objetos podem ser construídos também utilizando uma análise de ponteiros como o algoritmo de Andersen, sensível a contexto ou não (MØLLER; SCHWARTZ-BACH, 2015). As versões sensíveis a contexto são chamadas de k-CFA (Control Flow Analy-sis (CFA)). A versão insensível a contexto é chamada de 0-CFA. Essencialmente, a análise prossegue como o algoritmo de Andersen, mas o call graph é construído incrementalmente enquanto a análise descobre os tipos dos objetos para cada variável que o programa pode apontar. Mesmo a análise 0-CFA é considerada mais precisa que a RTA. Por exemplo, a Figura 2.5 apresenta um código onde o RTA assume que qualquer implementação do método get pode

(25)

2.3. ANÁLISE ESTÁTICA 24 ser chamado em qualquer lugar do programa. Entretanto, 0-CFA consegue distinguir as duas chamadas.

1 static class A { A get (A x) {return x;}}

2 static class D extends A { A get (A x) {return new A();}}

3 static class B extends A { A get (A x) {return new D();}}

4 static class C extends A { A get (A x) {return this;}}

5

6 public static void main(String[] args) {

7 A x = new A();

8 x = x.get (new B ()); 9 A y = new C() ;

10 y.get (x) ; 11 }

Figura 2.5: Exemplo de código no qual a análise 0-CFA consegue distinguir as chamadas do método get

Para coletar as informações de uso das coleções, este trabalho utiliza um call graph construído a partir do 0-CFA provido pela biblioteca WALA (WALA - T.J. Watson Libraries for Analysis, 2016). Para cada método do call graph foram percorridas todas as chamadas e para cada chamada foi construído um contexto baseado nos aninhamentos de loops onde os métodos das coleções eram chamados. Dessa forma, cada método é re-analizado para cada chamada, uma das características de uma análise sensível a contexto. Além disso, para saber se a chamada ao método da coleção está dentro do loop, é analisado o CFG do método que realizou a chamada. Assim, a análise realizada neste trabalho, apesar de ser realizada sobre um call graph construído a partir de um 0-CFA, adiciona características de sensibilidade a contexto e a fluxo. O próximo tópico aborda o WALA, enquanto o Capítulo 3 explica com mais detalhes a abordagem utilizada para a realizar a análise estática.

2.3.2 WALA

Para executar a análise estática das aplicações, este trabalho utilizou a Biblioteca WALA (WALA - T.J. Watson Libraries for Analysis, 2016). WALA dá suporte à análise estática de aplicações Java com base no seu bytecode. Tipicamente, utiliza-se WALA para realizar análises inter-procedurais, da seguinte forma:

1. Construir a hierarquia de classe (ClassHierarchy): ler o programa (por exemplo bytecode) na memória, e analisar algumas informações básicas em relação aos tipos representados. O objeto ClassHierarchy representa o universo do código a ser analisado.

(26)

2.3. ANÁLISE ESTÁTICA 25 2. Construir um call graph (CallGraph): realizar análise de ponteiros com a

cons-trução de call graphs para resolver os alvos das chamadas, e construir um grafo de objetos CGNode (nó de um call graph) que representam as possíveis chamadas do programa. A classe CallGraph representa potencialmente call graphs sensíveis a contexto. Cada nó do call graph representa um método (IMethod) dentro de um contexto. Note que, dado um método, um call graph sensível a contexto pode ter vários nós (contextos) representando o método.

3. Realizar análises estáticas no call graph criado no item anterior.

Vários tipos de análises estáticas são possíveis. Muitas das análises disponíveis utilizam a Representação Intermediária (IR) de código, a qual representa as instruções e o CFG de um método qualquer. A IR é um CFG das instruções na Static Single Assignment form (SSA form) (CYTRON et al., 1991). Este trabalho, em particular, realizou a análise inter-procedural conforme os passos enumerados anteriormente, e também navegou entre as IR de cada método para analisar o uso das coleções.

Além disso, WALA fornece um arcabouço para a análise de ponteiros flow-insensitive. Todas as implementações de análises ponteiro permitem a construção dos call graphs. Dentre elas, a ZeroCFA, fornece uma análise de ponteiros context-insensitive simples e pouco custosa. O WALA fornece um construtor padrão de call graph a partir da política ZeroCFA (0-CFA). A Figura 2.6 mostra um exemplo de código simplificado para criação de call graph através do WALA. O call graph é criado através de um contrutor para a ZeroCFA (linha 6), e realiza alguma análise sobre as instruções da representação intemerdiária dos métodos sincronizados (linha 18).

(27)

2.3. ANÁLISE ESTÁTICA 26

1 IClassHierarchy cha = ClassHierarchy.make(scope);

2 Iterable<Entrypoint> e = Util.makeMainEntrypoints(scope, cha); 3 AnalysisOptions o = new AnalysisOptions(scope, e);

4

5 CallGraphBuilder builder = Util.makeZeroCFABuilder(o, new

AnalysisCache(), cha, scope);

6 CallGraph cg = builder.makeCallGraph(o, null); 7

8 for (CGNode cgNode : cg.getEntrypointNodes()) {

9 Set<CGNode> nodes = cg.getNodes(cgNode.getMethod().getReference())

;

10 for (Iterator nodeIter = nodes.iterator(); nodeIter.hasNext();) {

11 final CGNode node = (CGNode) nodeIter.next();

12 CallSiteReference callsite = iter.next(); 13

14 IMethod method = cha.resolveMethod(callsite.getDeclaredTarget()

);

15 if(method.isSynchronized()){

16 IR ir = ir = cache.getIRFactory().makeIR(method, ...); 17 for (SSAInstruction instruction : ir.getInstructions()) {

18 ...

19 }

20 }

21 }

22 }

(28)

27 27 27

3

Abordagem

A abordagem elaborada neste trabalho tem por objetivo recomendar a troca entre im-plementações de coleções para melhorar o consumo de energia de uma aplicação de software, com base em uma análise simples. A principal vantagem da abordagem proposta é que, uma vez estabelecidos os perfis de consumo de energia das estruturas de dados para o ambiente no qual a aplicação irá rodar, ela é puramente estática. Assim, não há necessidade de executar a aplicação cujo consumo de energia se deseja reduzir.

Este capítulo tem como objetivo apresentar as etapas da abordagem proposta. O capítulo conta com uma seção que apresenta uma visão geral da abordagem (Seção 3.1). Uma seção sobre a medição dos perfis de consumo de energia das estruturas de dados (Seção 3.2). Outra seção sobre a análise das coleções usadas pela aplicação (Seção 3.3). Ainda, uma seção sobre a recomendação das estruturas de dados para redução do consumo de energia (Seção 3.4) e, finalmente, uma outra seção que apresenta a Collections Energy Consumption Optimization tool (CECOTOOL), que implementa a abordagem proposta (Seção 3.5).

3.1 Visão Geral da Abordagem

A abordagem proposta é composta por três etapas: 1. Análise dos perfis de consumo de energia;

2. Análise estática do uso das coleções pela aplicação; 3. Recomendação.

A Figura 3.1 descreve o fluxo da abordagem. O processo se inicia com a análise dos perfis de consumo de energia das coleções. O perfil do consumo de energia é a agregação das informações sobre consumo de energia para as várias operações de uma dada implementação. Esta etapa só precisa ser realizada uma vez para cada plataforma de execução (hardware + SO) ou quando muda-se a versão de Java. Em paralelo, é realizada a etapa da análise estática da aplicação, que tem como saída a análise do uso das coleções. A etapa da recomendação é responsável por indicar implementações de coleções que provavelmente reduzirão o consumo de

(29)

3.2. PERFIS DE CONSUMO DE ENERGIA DAS COLEÇÕES 28 energia da aplicação. Ela recebe como entrada a saída da etapa da análise do uso das coleções e os perfis de consumo de energia das coleções. Ao final do processo, as recomendações são realizadas. As próximas seções detalham cada uma dessas etapas.

Análise Estática do Uso das Coleções

pela Aplicação Recomendação de Uso das Implementações de Coleções Entrada da análise Entrada da recomendação Uso das Coleções Saída da análise Perfis de Consumo das Coleções Bytecode Aplicação Análise do Perfil de Consumo Coleções Recomendações

Figura 3.1: Fluxo da abordagem

3.2 Perfis de Consumo de Energia das Coleções

A medição dos perfis de consumo de energia das coleções é um pré-requisito para a abordagem aplicada neste trabalho. Embora seja possível coletar essa informações para qualquer coleção de interesse, este trabalho foca em implementações seguras a threads, representada por listas, mapas e conjuntos, e suas respectivas interfaces: List, Map e Set. Para cada implementação de coleção é medido o consumo de energia das operações de inserção, leitura e remoção. Esses perfis são entradas para a CECOTOOL e só é necessário realizar a medição dos perfis uma vez por ambiente no qual a aplicação rodar. A Tabela 3.1 apresenta as estruturas de dados e operações que têm o seu consumo de energia medido neste trabalho. Contudo, a abordagem não é específica apenas para essas.

Tabela 3.1: Coleções, implementações e métodos

Coleção Estrutura de dados Operações

List Vector, synchronizedList,

CopyOnWriteArrayList

add(), iterator(), get(), remove()

Map Hashtable, ConcurrentHashMap,

synchronizedMap, ConcurrentS-kipListMap

add(), get(), re-move()

Set ConcurrentHashSet,

synchroni-zedSet, CopyOnWriteArraySet,

ConcurrentSkipListSet

add(), get(), re-move()

(30)

3.3. ANÁLISE ESTÁTICA DAS APLICAÇÕES 29 Para medir o perfil de consumo de energia das coleções são utilizados os Energy Profile Benchmarks(EPB) criados por Pinto et al. (2016). Os EPBs são benchmarks que servem para aferir o consumo de energia e desempenho de 16 implementações de coleções seguras a threads. Para cada coleção é analisado o perfil de consumo de energia das operações de inserção, remoção e leitura simulando um ambiente concorrente e com intenso uso dessas coleções. Para medir o consumo de energia, foi utilizada a biblioteca jRAPL (LIU; PINTO; LIU, 2015). Como resultado dessas medições, tem-se o perfil de consumo de energia de cada coleção. Esses perfis serão utilizados posteriormente para realizar a recomendação de qual melhor implementação de coleção utilizar. Além disso, esses perfis de consumo de energia são específicos para a máquina em que a aplicação vai rodar. Dessa forma, caso as aplicações rodem em hardware com configurações diferentes, é necessário novamente medir esses perfis de consumo de energia. Os perfis dependem do ambiente devido às diferenças entre plataformas de hardware e variações entre as versões da Máquina Virtual Java (JVM). Já foi demonstrado (PINTO et al., 2016), por exemplo, que o consumo de energia e o tempo de execução da classe ConcurrentHashMap mudaram significativamente entre as versões 7 e 8 da linguagem Java.

3.3 Análise Estática das Aplicações

Essa seção explica a etapa da análise estática, a qual extrai informações das aplicações, como a frequência de uso das operações e o contexto no qual essas operações são chamadas.

Para ilustrar as próximas etapas, a Figura 3.2 apresenta um exemplo de código, retirado da biblioteca Xalan (Xalan - The Apache XML Project, 2016), que realiza inserções e leituras na variável imports do tipo Vector. Esse exemplo será referenciado ao longo do texto como “Exemplo Base”. O código foi simplificado para facilitar o entendimento.

Nesse exemplo, a variável imports (variável coleção) da classe Stylesheet é uma coleção de objetos na qual são realizas inserções e leituras (linhas 6 e 10 respectivamente). A classe Stylesheet possui os métodos getImports e addImports (linhas 13 e 20), que chamam os métodos setImport e getImport para realizar as inserções e leituras na variável imports. Estes dois métodos são chamados dentro de loops. Além disso, a variável imports também pode ser lida a partir do método recomposeImports() (linha 34), que por sua vez é chamado pelo método recompose(). Neste caso a operação de leitura, elementAt, está sendo realizada dentro de dois loops aninhados (linha 28 e linha 35). Logo, para a variável importstem-se (1) um método para inserção sendo chamado dentro de apenas um loop e (2) duas ocorrências de leitura, uma dentro de um loop e outra dentro de dois loops aninhados. Apesar de estarem em métodos diferentes, o aninhamento é levado em consideração por esta abordagem.

(31)

3.3. ANÁLISE ESTÁTICA DAS APLICAÇÕES 30

1 public class Stylesheet extends ElemTemplateElement{

2 ...

3 private Vector imports;

4

5 public void setImport(StylesheetComposed v){

6 imports.addElement(v);

7 }

8

9 public StylesheetComposed getImport(int i){

10 return (StylesheetComposed) imports.elementAt(i);

11 }

12

13 protected void getImports(Stylesheet stylesheet){

14 for (int i = 0; i < n; i++)

15 {

16 Stylesheet imported = stylesheet.getImport(i);

17 }

18 }

19

20 public void addImports(Stylesheet stylesheet, ...){

21 for (int i = 0; i < n; i++)

22 {

23 stylesheet.setImport(style);

24 }

25 }

26

27 public void recompose() throws TransformerException{

28 for (int i = 0, j= importList.size() -1; i < importList.size();

i++) 29 { 30 globalImportList[j--].recomposeImports(); 31 } 32 } 33 34 void recomposeImports(){ 35 while (count > 0)

36 m_endImportCountComposed += this.getImport(--count);

37 }

38 ...

39 }

(32)

3.3. ANÁLISE ESTÁTICA DAS APLICAÇÕES 31 3.3.1 Estimativa da Frequência de Uso

A estimativa da frequência de uso das operações de inclusão, remoção e consulta é realizada através de análise estática do bytecode das aplicações. Essa análise estática é inter-procedural.

A análise inter-procedural utiliza call graphs e control flow graphs para medir a quanti-dade de ocorrências das operações das coleções, além do obter o nível de aninhamento de loops em que essas operações são chamadas. Para cada método é verificado se existem chamadas a operações das coleções estudadas nesse trabalho (e disponíveis na Tabela 3.1). A quantidade de chamadas a cada uma é armazenada em um contador. Ao final de todo o fluxo de chamadas do call graph, obtém-se o número de vezes que estas operações são chamadas. Como podem existir vários caminhos a serem percorridos para uma determinada chamada, a contagem de ocorrências está relacionada a um determinado contexto de execução. Esse contexto é definido pelas seguintes variáveis:

 Tipo da coleção;

 Nome da variável que armazena a coleção na qual é realiza a operação;

 Número de ocorrências da operação;

 Nome da operação;

 Classe na qual a operação é realizada;

 Método no qual a operação é realizada;

 Linha de código da ocorrência da operação;

 Informações sobre os aninhamentos dos loops.

A Tabela 3.2 apresenta o contexto para a operação elementAt da variável imports do Exemplo Base, quando essa tem sua ocorrência dentro apenas de um loop. Enquanto a Tabela 3.3 apresenta o contexto para a mesma operação, quando chamada dentro de dois loops aninhados.

Portanto, chamadas à mesma operação podem ser contadas de formas diferentes de acordo com os contextos nos quais se encontram. Como percebe-se no Exemplo Base, a operação elementAt possui dois contextos: um no qual foi chamada dentro de um loop e outra no qual foi chamada dentro de dois loops aninhados.

3.3.2 Análise de Loops

A simples contagem das ocorrências das chamadas das operações não é suficiente para realizar uma boa recomendação, uma vez que essa contagem não representa o real uso das

(33)

3.3. ANÁLISE ESTÁTICA DAS APLICAÇÕES 32 Tabela 3.2: Exemplo de contexto aplicado à operação elementAt

Nome da variável imports

Nome da operação elementAt

Número de ocorrências 1

Tipo da estrutura de dados Vector

Classe na qual a operação foi realizada Stylesheet Método no qual a operação foi realizada getImport

Linha de código 10

Nível de Aninhamento do loop 1

Tabela 3.3: Exemplo de contexto aplicado à operação elementAt, dentro de dois loops aninhados

Nome da variável imports

Nome da operação elementAt

Número de ocorrências 1

Tipo da estrutura de dados Vector

Classe na qual a operação foi realizada Stylesheet Método no qual a operação foi realizada getImport

Linha de código 10

Nível de Aninhamento do loop 2

operações devido à quantidade de vezes que as operações podem ser executadas dentro de um loop. Desta forma, quando uma operação é chamada dentro de um loop, uma contagem mais realista seria multiplicar a quantidade de ocorrências pela quantidade de iterações do loop. Porém, na maioria das vezes a quantidade de iterações dos loops não pode ser determinada de forma precisa estaticamente, podendo, inclusive, variar ao longo da execução do programa.

A predição do número exato de iterações do loop é impossível, dado que é equivalente a resolver o problema da parada (RICE, 1953). Por exemplo, o loop foreach, muito comum em aplicações Java (DYER et al., 2013), itera sobre os elementos das estruturas de dados que implementam a interface Iterable. Desta forma ele percorre a estrutura de dados até o seu fim ou encontrar uma condição de saída. Isto torna difícil predizer quantas vezes esse loop executará, uma vez que o tamanho da estrutura de dados não está explicitamente disponível. A Figura 3.3 apresenta um exemplo real retirado da aplicação Tomcat do uso do foreach, no qual ele itera sobre o conjunto de chaves de um mapa. Vários estudos, por exemplo, (RODRIGUES, 2014), (HEALY et al., 1998), (WU; LARUS, 1994) e (BALL; LARUS, 1993), tratam sobre a predição de loops, no entanto, estes trabalhos estão longe de cobrir todos os possíveis usos dos loops de uma linguagem de alto nível, como Java.

Como abordagens existentes para estimar a número de iterações de um loop normalmente exigem a execução do programa (RODRIGUES, 2014), este trabalho não considera estas aborda-gens, uma vez que o objetivo aqui é analisar o consumo de energia de forma estática. Parte destes trabalhos levam em conta o nível de aninhamento dos loops como uma forma de dar peso às operações que são realizadas dentro de loops. Embora esta abordagem não estipule quantas vezes

(34)

3.4. RECOMENDAÇÃO 33 uma operação é invocada, resultados experimentais deste trabalho (mais na Seção 4) sugerem que, na prática, ela é eficiente para estabelecer que uma operação é invocada uma quantidade de vezes potencialmente maior que outra. Assim, este trabalho atribui pesos aos diferentes loops que chamam as operações das estruturas de dados, os quais são responsáveis por influenciar na recomendação de qual implementação de coleção usar.

1 for (String name : virtualMappings.keySet()) 2 {

3 File file = virtualMappings.get(name);

4 NamingEntry entry = new NamingEntry(name, new FileResource(file), 5 NamingEntry.ENTRY);

6 virtual.add(entry); 7 }

Figura 3.3: Exemplo de utilização do loop foreach

A análise do nível de aninhamento dos loops, como mencionado antes, é inter-procedural. Logo, o aninhamento de loops é levado em conta mesmo quando os loops aparecem em métodos distintos. Dessa forma, foram coletadas informações sobre o nível de aninhamento dos loops externos às operações. Uma chamada dentro de loops terá um peso atribuído baseado no nível de aninhamento desses loops. Para exemplificar este conceito, a Figura 3.4-(a) apresenta a operação addcom dois loops externos. Assim, seu nível de aninhamento é 2. Já na Figura 3.4-(b), o método add apresenta apenas um loop externo, então seu nível de aninhamento é 1. O nível de aninhamento do loop é levado em conta mesmo quando os loops estão em métodos diferentes pois isso torna as estimativas mais precisas.

Figura 3.4: Exemplo de nível aninhamento de loops

1 for (i=0; i<n ; i++) 2 { 3 for (j=0; j<n ; j++) 4 { 5 imports.add(i); 6 } 7 }

Nível de aninhamento dos loops = 2

1 for (i=0; i<n ; i++) 2 {

3 imports.add(i); 4 }

Nível de aninhamento do loop = 1

3.4 Recomendação

A recomendação de qual implementação de coleção usar é baseada em uma fórmula que tem como entrada as informações coletadas dos perfis de consumo de energia das coleções em

(35)

3.4. RECOMENDAÇÃO 34 um determinado ambiente e o uso das coleções pela aplicação. Assim, o fator de consumo para cada operação realizada sobre a variável de uma coleção (por exemplo, a variável imports no Exemplo Base – Seção 3.3) é:

f atorConsumoOperacao= P ×U 3.1

Onde,

 P = Perfil de consumo de energia para a operação da implementação de coleção

 U = Peso do Uso

O perfil de consumo da operação é obtido através da execução do EPBs que medem os de consumos de energia para cada operação de uma estrutura de dados (Seção 3.2).

O Peso do Uso é baseado na seguinte regra:

1. Caso a variável coleção não esteja dentro de um loop, o Peso do Uso será igual ao número de ocorrências da operação específica da variável coleção (#ocorrencias). 2. Caso a variável coleção esteja dentro de loops, o Peso do Uso é igual a

(#ocorrencias + 1)(x+1), onde “x” é o nível de aninhamento dos loops. Os valores, #ocorrências e nível de aninhamento dos loops são somados a 1 para evitar que o peso seja neutro, mesmo quando houver loop. Por exemplo, quando uma operação tiver apenas uma ocorrência e estiver dentro de um loop apenas.

O fator de consumo total de uma variável coleção, é calculado a partir da soma dos somatórios do fatores de consumo de energia de cada operação realizada sobre ela:

f atorConsumoTotal=

( f coi) +

( f cot) +

( f cor)  3.2 Onde,  fco = fatorConsumoOperacao  i = Inserção  t = Leitura  r = Remoção

Após o cálculo do total, tem-se o fatorConsumoTotal para cada estrutura de dados. Quanto menor o fator, mais fortemente recomendado é o uso dessa estrutura de dados.

Exemplo de Aplicação da Fórmula para Recomendação

A Tabela 3.4 apresenta os valores para cada operação da variável coleção imports do Exemplo Base quando seu tipo for Vector. A operação elementAt possui duas ocorrências,

(36)

3.4. RECOMENDAÇÃO 35 uma vez dentro de um loop (nível de aninhamento 1), e outra dentro de dois loops (nível de aninhamento 2). Ainda, apresenta uma ocorrência da operação addElement dentro de um loop (nível de aninhamento 1). Para o Exemplo Base, nenhuma operação de remoção foi realizada.

Tabela 3.4: Valores para a variável coleção imports quando seu tipo for Vector

Operação Perfil de Consumo Tem loop Nível de Aninhamento do Loop Peso do Uso

addElement 8,25 Sim 1 22= 4

elementAt 7,2 Sim 1 22= 4

elementAt 7,2 Sim 2 23= 8

Aplicando a fórmula:

f atorConsumoTotal= 8, 25 × 4 + (7, 2 × 4 + 7, 2 × 8) + 0 = 33 + 28, 8 + 57, 6 = 119, 4 3.3

Dessa forma o resultado para a variável coleção imports, quando seu tipo é Vector, é igual a 119,4. Esse resultado apresentado pela aplicação da fórmula não é o consumo de energia da respectiva implementação de coleção, mas sim uma métrica que servirá para comparar as estruturas de dados mais energeticamente eficientes. Esse valor é proporcional ao consumo de energia. Assim, a implementação que apresentar o menor valor será recomendada. Por outro lado, a Tabela 3.5 apresenta os valores para variável imports, rodando no mesmo ambiente, quando seu tipo for substituído por synchronizedList.

Tabela 3.5: Valores para a variável coleção imports quando seu tipo for synchronizedList

Operação Perfil de Consumo Tem loop Nível de Aninhamento do Loop Peso do Uso

addElement 8,56 Sim 1 22= 4

elementAt 5,67 Sim 1 22= 4

elementAt 5,67 Sim 2 23= 8

Como se trata da mesma variável coleção, a mudança ocorre somente no valor do perfil de consumo de energia do novo tipo da variável, synchronizedList. A seguir é apresentado a fórmula aplicada novamente:

f atorConsumoTotal= 8, 56 × 4 + (5, 67 × 4 + 5, 67 × 8) + 0 = 34, 24 + 22, 68 + 45, 36 = 102, 28  3.4 Esse resultado sugere que, se a variável coleção imports for implementada utilizando o tipo synchronizedList, esta provavelmente irá consumir menos energia do que utilizando Vector. Por isso, a ferramenta recomendará que seja utilizado o tipo synchronizedList.

(37)

3.5. IMPLEMENTAÇÃO 36

3.5 Implementação

Para a realização deste trabalho foi criada a ferramenta Collections Energy Consumption Optimization(CECOTOOL). Esta ferramenta é composta por dois módulos e possui aproximada-mente 1200 linhas de código Java. O primeiro é responsável por realizar a análise estática de uso das estruturas de dados das aplicações. O segundo, por sua vez, realiza as recomendações das estruturas de dados, com base nos perfis de consumo de energia das estruturas de dados e na análise de uso das estruturas de dados.

3.5.1 CECOTOOL- Análise de Uso das Coleções

A ferramenta realiza a análise inter-procedural das aplicações utilizando a biblioteca WALA - T.J. Watson Libraries for Analysis (2016). WALA dá suporte à análise estática de aplicações

Java com base no seu bytecode. Esta ferramenta detecta as instâncias tanto de variáveis globais quanto de variáveis locais utilizadas. A análise inter-procedural utiliza call graph e control flow graphcriados pelo WALA para coletar as informações de uso das coleções e seus contextos.

Mais especificamente, este trabalho utiliza um call graph construído a partir do 0-CFA provido pela biblioteca WALA. Para cada método do call graph, foram percorridas todas as chamadas e para cada chamada foi construído um contexto baseado nos aninhamentos de loops onde os métodos das coleções eram chamados. Dessa forma, cada método é re-analizado para cada chamada, uma das características de análises sensível a contexto. Além disso, para saber se a chamada ao método da coleção está dentro do loop, é analisado o CFG do método que realizou a chamada. Assim, a análise realizada neste trabalho, apesar de ser realizada sobre um call graph construído a partir de um 0-CFA, adiciona características de sensibilidade a contexto e a fluxo. Além de coletar informações sobre o uso das coleções, também são coletadas informações sobre os loops externos às operações detectadas. Para identificar os loops e suas informações, como início, fim e blocos, foi utilizada a biblioteca walautil (2016), escrita em Scala. Esta biblioteca possui uma série de classes utilitárias para o WALA. Neste trabalho foi utilizada especificamente a classe LoopUtil.

Tecnicamente, o call graph foi criado utilizando o construtor de call graphs do WALA, Util.makeZeroCFABuilder, passando as estruturas de análise: a AnalyisScope, que representa o código da aplicação a ser analisada, e a ClassHierarchy, que representa a hierarquia de classes da aplicação. O ponto de entrada de cada call graph foi definido por padrão como o método main() da aplicação, Util.makeMainEntrypoints, porém é possível definir outros métodos como ponto de entrada. Para cada nó CGNode do call graph percorre-se todos os call sites, node.iterateCallSites(). Para cada call site, CallSiteReference, é analisado seu método alvo (MethodReference), obtido por callsite.getDeclaredTarget().

Para cada método analisado da aplicação, foi criada sua representação intermediária (IR). A IR é uma estrutura de dados que representa as instruções de um método. Ela é criada a partir

(38)

3.5. IMPLEMENTAÇÃO 37 do bytecode e representa as instruções do método em uma linguagem próxima ao bytecode Java, porém baseada em Static Single Assignment – SSA form (CYTRON et al., 1991). A forma SSA é uma propriedade da IR que requer que cada variável seja atribuída exatamente uma vez e toda variável é definida antes de ser utilizada. Além disso, organiza as instruções em um control-flow graph (GFG) em blocos de instruções. A análise das estruturas de dados é feita a partir da representação intermediaria de cada método. O Apêndice A apresenta um exemplo de IR do método setImports do Exemplo Base. Assim, são coletadas as informações sobre chamada de operações da coleção.

Além das informações fornecidas pela biblioteca walautil (walautil, 2016), para realizar o cálculo do nível de aninhamento dos loops, foi necessário coletar as informações de profundidade de cada loop e verificar se as chamadas às operações das coleções estão ocorrendo dentro de loops. Assim, para cada chamada a um método, é passado um contexto formado pelos loops e suas profundidades. O nível de profundidade do loop é contado a partir do ponto de entrada da análise (método main()). Para verificar se uma operação de coleção está sendo chamada dentro de loop, são coletadas as informações para cada loop através do biblioteca walautil a partir da IR do método, por exemplo, pegar o head do loop (LoopUtil.getLoopHeaders(IR)). Então é verificado se as chamadas são realizadas dentro de algum dos blocos do loop. Toda esta análise é feita recursivamente.

Como resultado do processo da análise estática, são retornados: tipo, nome da variável, nome do método, linha de código, número de ocorrências, profundidade dos loops externos, além de informações de contexto como classe, método no qual a operação se encontra. A Tabela 3.6 apresenta a saída da análise estática da aplicação para o Exemplo Base (Figura 3.2)

Tabela 3.6: Exemplo de saída da análise do uso das estrutura de dados

Tipo Método da chamada Variável Operação Linha de Código Classe Ocorrências Nível de aninhamento dos Loops Vector addImports imports addElement 6 Stylesheet 1 1

Vector getImport imports elementAt 10 Stylesheet 1 1 Vector getImport imports elementAt 10 Stylesheet 1 2

3.5.2 CECOTOOL- Recomendação

Este módulo é responsável pela recomendação de quais implementações de coleção usar dentre as coleções analisadas. Essa recomendação é derivada do resultado obtido após aplicação da fórmula para recomendação (Seção 3.4). O objetivo da recomendação é apontar para o desenvolvedor possíveis pontos de redução do consumo de energia da aplicação.

Para realizar a recomendação, a ferramenta recebe como entrada o conjunto de infor-mações de uso das coleções, provido pela análise estática da aplicação, e os perfis de consumo de energia para cada coleção. A partir dessas informações, para cada variável é apresentado o valor do fator de consumo de energia para cada implementação de coleção correspondente. Quanto maior o valor, maior a tendência daquela variável consumir mais energia conforme

(39)

3.5. IMPLEMENTAÇÃO 38 explicado anteriormente. A Tabela 3.7 apresenta um exemplo de saída de recomendação desta ferramenta. Neste caso, o tipo recomendado para a variável imports do Exemplo Base será synchronizedList.

Tabela 3.7: Exemplo da saída da recomendação

Variável Tipo Fator Consumo de Energia

imports Vector 119,4

(40)

39 39 39

4

Avaliação

Este capítulo tem como objetivo apresentar a metodologia para a execução dos experi-mentos e os resultados coletados. O capítulo conta com uma seção sobre a metodologia realizada (Seção 4.1) e uma outra seção sobre os resultados obtidos (Seção 4.2).

4.1 Metodologia

Nesta seção são apresentados os benchmarks utilizados nos experimentos e as trans-formações realizadas nestes. Também são explicadas as configurações dos ambientes onde os experimentos foram realizados, e a metodologia empregada e as etapas necessárias para a execução dos experimentos.

4.1.1 Benchmarks

Neste trabalho foram utilizados dois benchmarks pertencentes à suíte de benchmarks DaCapo (BLACKBURN et al., 2006). Os benchmarks selecionados foram o Xalan e o Tomcat. Estes benchmarks foram escolhidos pois (1) são aplicações reais, (2) ambos possuem intenso uso de estruturas de dados concorrentes e (3) são multithreaded, pré-requisito para o estudo. Em particular:

 O Xalan (Xalan - The Apache XML Project, 2016) é um processador XSLT que

transforma documentos XML em HTML, texto ou em outros formatos. O benchmark é multithreaded, uma vez que utiliza múltiplas threads para realizar seu trabalho. Este número é definido de acordo com o número de processadores disponíveis na CPU. Cada thread transforma um arquivo XML, que é representado por um elemento em uma fila. O workload do benchmark utiliza 19 diferentes arquivos entrada. Cada arquivo é inserido 1.000 vezes na fila. O benchmark, então, processa um total de 19.000 arquivos XML. A versão utilizada do Xalan no benchmark foi a 2.7.1 com aproximadamente 353 mil linhas de código.

 Apache Tomcat (Apache Tomcat, 2016) é um servidor Web Java, que implementa

Referências

Documentos relacionados

Este trabalho buscou, através de pesquisa de campo, estudar o efeito de diferentes alternativas de adubações de cobertura, quanto ao tipo de adubo e época de

Apresenta-se neste trabalho uma sinopse das espécies de Bromeliaceae da região do curso médio do rio Toropi (Rio Grande do Sul, Brasil), sendo também fornecida uma chave

O objetivo do curso foi oportunizar aos participantes, um contato direto com as plantas nativas do Cerrado para identificação de espécies com potencial

esta espécie foi encontrada em borda de mata ciliar, savana graminosa, savana parque e área de transição mata ciliar e savana.. Observações: Esta espécie ocorre

O valor da reputação dos pseudônimos é igual a 0,8 devido aos fal- sos positivos do mecanismo auxiliar, que acabam por fazer com que a reputação mesmo dos usuários que enviam

Apesar dos esforços para reduzir os níveis de emissão de poluentes ao longo das últimas décadas na região da cidade de Cubatão, as concentrações dos poluentes

A assistência da equipe de enfermagem para a pessoa portadora de Diabetes Mellitus deve ser desenvolvida para um processo de educação em saúde que contribua para que a

servidores, software, equipamento de rede, etc, clientes da IaaS essencialmente alugam estes recursos como um serviço terceirizado completo...