• Nenhum resultado encontrado

Otimizando sistemas intensivos em E/S através de programação concorrente

N/A
N/A
Protected

Academic year: 2021

Share "Otimizando sistemas intensivos em E/S através de programação concorrente"

Copied!
162
0
0

Texto

(1)

Otimizando Sistemas Intensivos em E/S Através de Programação Concorrente

Por

Saulo Medeiros de Araújo Dissertação de Mestrado

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

(2)

OTIMIZANDO SISTEMAS INTENSIVOS EM E/S ATRAVÉS DE PROGRAMAÇÃO CONCORRENTE

Este trabalho foi apresentado à Pós-Graduação em Ciência da Computação do Centro de Informática da Universidade Federal de Pernambuco como requisito parcial para obtenção do grau de Mestre em Ciência da Computação.

Orientador:

Silvio Romero de Lemos Meira

Recife 2015

(3)

Catalogação na fonte

Bibliotecária Joana D’Arc Leão Salvador CRB4-532

A663o Araújo, Saulo Medeiros de.

Otimizando sistemas intensivos em E/S através de programação concorrente / Saulo Medeiros de Araújo. – Recife: O Autor, 2015.

161 f.: fig., tab.

Orientador: Silvio Romero de Lemos Meira.

Dissertação (Mestrado) – Universidade Federal de Pernambuco. CIN,

Ciência da Computação, 2015. Inclui referências e apêndices.

1. Engenharia de software. 2. Programação paralela (Computação). 3. Programação orientada a objetos (Computação). 4. Banco de dados relacionados. I. Meira, Silvio Romero de Lemos (Orientador). II. Titulo. 005.1 CDD (22. ed.) UFPE-MEI 2015-107

(4)

em Ciência da Computação do Centro de Informática da Universidade Federal de Pernambuco, sob o título “Otimizando Sistemas Intensivos em E/S Através de Programação Concorrente”, orientada pelo Prof. Silvio Romero de Lemos Meira e

aprovada pela Banca Examinadora formada pelos professores:

______________________________________________ Prof. Augusto Cézar Alves Sampaio

Centro de Informática/UFPE

______________________________________________ Prof. Jones Oliveira de Albuquerque

Departamento de Estatística e Informática / UFRPE

_________________________________________________ Prof. Silvio Romero de Lemos Meira

Centro de Informática / UFPE

Visto e permitida a impressão. Recife, 6 de abril de 2015.

___________________________________________________

Profa. Edna Natividade da Silva Barros Coordenadora da Pós-Graduação em Ciência da Computação do Centro de Informática da Universidade Federal de Pernambuco.

(5)
(6)

Agradecimentos

Quero agradecer, em primeiro lugar, à minha esposa, Karina. Sem o seu apoio incondicional, não teria ido tão longe. Agradeço também à minha filha, Alice. Sua chegada, no início desta pesquisa, encheu-nos de alegria.

Diversos outros agradecimentos são devidos. Ao meu pai, Stelio, que me apresentou ao computador e, ainda mais importante, ao violão, quando eu ainda era criança. À minha mãe, Henrriete, que foi minha primeira professora de Matemática e, portanto, de Ciência da Computação. À minha irmã Stella, que deu, por mim, o primeiro passo para que esta pesquisa fosse possível. À minha sogra Tânia, que, assim como meus pais, nos ajudou a cuidar de Alice.

Ao meu amigo e, agora, orientador, Silvio. Quase duas décadas depois de nossa primeira colaboração, seu entusiasmo continua sendo uma poderosa força motriz para mim. Aos professores Kiev e Nelson, cuja participação foi fundamental para a publicação de alguns dos resultados alcançados nesta pesquisa. Ao professor Airton, que, com muita paciência, me ensinou Análise Real.

Aos meus grandes amigos Fábio, Idevan, Celso e Humberto. Esta pesquisa é, também, fruto de tudo o que aprendemos juntos na NEWStorm e na Pitang.

Por fim, gostaria de agradecer ao Banco Central do Brasil, que patrocinou esta pesquisa, e aos colegas com quem trabalho, os quais assumiram minhas funções durante minha ausência.

(7)

The second season I am to know” (Page - Plant, The Rain Song)

(8)

Resumo

ORMs (Object-Relational Mappers) são bastante populares porque eles reduzem o esforço de desenvolvimento de camadas de acesso a dados ao permitir, entre outras coisas, que sistemas manipulem objetos transientes e persistentes de maneira similar. Em particular, ORMs permitem que sistemas naveguem por objetos de ambos os tipos exatamente da mesma maneira. Infelizmente, entretanto, navegar por objetos persistentes é muito mais lento do que navegar por objetos transientes. Para atenuar este problema, ORMs pré-carregam objetos executando consultas SQL (Structured Query Language) que, no lugar de carregar os atributos de um único objeto, tal como ocorre quando objetos são carregados sob demanda, carregam os atributos de vários objetos. Em muitos casos, estas consultas podem ser executadas concorrentemente. Entretanto, a maioria dos ORMs executa consultas apenas sequencialmente.

Esta pesquisa visa aumentar o desempenho de sistemas baseados em ORMs. Para tanto, ela define uma DSL (Domain-Specific Language) de especificação de navegações por objetos chamada Litoral. Também integra esta pesquisa o projeto e a implementação de um interpretador de especificações Litoral. O interpretador navega por objetos transientes (aqueles que existem apenas na memória primária) e persistentes (aqueles que armazenados em um banco de dados relacional) e pré-carrega os do segundo tipo executando consultas sequencialmente ou concorrentemente.

A estratégia desta pesquisa foi avaliada com os benchmarks sintéticos Emeio e OO7, desenvolvidos, respectivamente, no contexto desta pesquisa e por terceiros. No primeiro, pré-carregar objetos executando consultas concorrentemente aumentou a velocidade de execução em até 323,6%. No segundo, o aumento foi de até 245,7%. Os benchmarks também foram implementados com os ORMs Hibernate e EcliseLink JPA, os quais aderem à especificação JPA (Java Persistence Architecture). O primeiro foi escolhido por ser bastante popular. O segundo foi escolhido por ser a implementação de referência desta especificação. As implementações baseadas no Hibernate e EclipseLink JPA foram significativamente otimizadas. Entretanto, em todos os cenários de Emeio e OO7 que oferecem oportunidades para pré-carregar objetos executando consultas concorrentemente, o desempenho delas foi inferior ao da implementação baseada no interpretador de Litoral. Palavras-chaves: Mapeamento objeto-relacional. Pré-carregamento. Concorrência.

(9)

Abstract

ORMs (Object-Relational Mappers) are quite popular because they reduce the effort of developing data access layers by allowing, among other things, systems manipulate transient and persistent objects in similar ways. In particular, ORMs allow systems navigate through objects of both types exactly the same way. Unfortunately, however, navigating through persistent objects is much slower than navigating through transient ones. To alleviate this problem, ORMs prefetch objects executing SQL (Structured Query Language) queries that fetch the attributes of multiple objects. In many cases, these queries can be executed concurrently. However, most ORMs execute queries sequentially only.

In this research, we aim to increase the performance of ORM based systems. To this end, we define a DSL (Domain-Specific Language) for specifying navigations through objects called Litoral. We also implement a Litoral interpreter that navigates through transient (objects that exist only in the primary memory) and persistent objects (objects stored in a relational database) and prefetches the second type with queries executed sequentially or concurrently.

We evaluated our strategy with the synthetic benchmarks Emeio and OO7. In the first one, prefetching objects with queries concurrently executed increased execution speed up to 323.6%. In the second one, the increase was up to 245.7%. We also implemented the benchmarks with the Hibernate and EcliseLink JPA ORMs, which adhere to the JPA (Java Persistence Architecture) specification. We chose the first one because it is quite popular and the second one because it is the reference implementation of JPA. We optimized the implementations based on Hibernate and EclipseLink JPA extensively. However, in all scenarios of Emeio and OO7 that offer opportunities for prefetching objects with queries concurrently executed, their performance was inferior to the performance of the implementations based on the Litoral interpreter.

(10)

Lista de ilustrações

Figura 1 – Trecho de código Java que navega por objetos. . . 17

Figura 2 – Especificação Litoral que declara navegações de mensagens para seus remetentes e destinatários. . . 18

Figura 3 – Trecho de código Java que pré-carrega objetos com o interpretador de Litoral. . . 18

Figura 4 – Trecho de código Java que requisita objetos especificando o predicado que eles devem satisfazer. . . 26

Figura 5 – Consultas SQL que ORMs executam para carregar sob demanda os atributos de mensagens manipulados pelo trecho de código da Figura 4. 26 Figura 6 – Consulta SQL que pré-carrega todos os atributos do primeiro tipo de uma mensagem. . . 27

Figura 7 – Consulta SQL que pré-carrega todos os atributos do tipo I de uma mensagem e, através de uma junção, também seu remetente. . . 27

Figura 8 – Consulta SQL que pré-carrega todos os atributos de uma mensagem. . 29

Figura 9 – Consultas que pré-carregam os destinatários e os remetentes de várias mensagens através do operador in de SQL. . . 29

Figura 10 – Trecho de código Java que informa a ORMs compatíveis com a especifi-cação JPA quais objetos eles devem pré-carregar. . . 30

Figura 11 – Método Java que carrega as mensagens enviadas por um usuário. . . . 33

Figura 12 – Método Java que carrega as mensagens enviadas por um usuário e pré-carrega seus remetentes. . . 33

Figura 13 – Gramática de Litoral em notação EBNF e em diagramas de sintaxe. 34 Figura 14 – Trecho de código Java que navega por objetos. . . 35

Figura 15 – Especificação Litoral que navega através de um único atributo. . . . 36

Figura 16 – Geração da especificação da Figura 15 a partir da gramática de Litoral. 36 Figura 17 – Especificação Litoral que navega através de coleções de objetos. . . . 36

Figura 18 – Especificação Litoral que navega através de mais de um atributo. . . 37

Figura 19 – Geração da especificação da Figura 18 a partir da gramática de Litoral. 37 Figura 20 – Especificação Litoral que navega por um único atributo. . . 37

Figura 21 – Especificação Litoral que navega indiretamente. . . 38

Figura 22 – Especificação Litoral que encadeia navegações indiretas. . . 38

Figura 23 – Especificação Litoral que replica uma navegação. . . 39

Figura 24 – Especificação Litoral que define uma travessia. . . 39

Figura 25 – Geração da especificação da Figura 24 a partir da gramática de Litoral. 40 Figura 26 – Especificação Litoral que define uma travessia recursiva. . . 41

(11)

Figura 29 – Representação de especificações Litoral no interpretador definidor. . 43

Figura 30 – Criação de uma especificação Litoral no interpretador definidor. . . . 44

Figura 31 – Analisador semântico. . . 45

Figura 31 – Analisador semântico - continuação . . . 46

Figura 31 – Analisador semântico - continuação. . . 47

Figura 31 – Analisador semântico - continuação. . . 48

Figura 32 – Teorema que assegura que o analisador semântico do interpretador definidor sempre é capaz de validar especificações Litoral em um número finito de recursões. . . 50

Figura 33 – Interpretador definidor. . . 51

Figura 33 – Interpretador definidor - continuação. . . 52

Figura 33 – Interpretador definidor - continuação. . . 53

Figura 34 – Função filter que impede navegações. . . 54

Figura 35 – Semântica 1. . . 55

Figura 36 – Especificação Litoral que entra em recursão infinita na semântica 1. . 55

Figura 37 – Mensagem que se auto-referencia. . . 55

Figura 38 – Semântica 2. . . 56

Figura 39 – Teorema que assegura que, sob a semântica 2, especificações Litoral não entram em recursão infinita. . . 57

Figura 40 – Programa Litoral que especifica navegações que não são realizadas na semântica 2. . . 57

Figura 41 – Especificação Litoral que permuta os passos de uma outra especifi-cação e, na semântica 2, navega por objetos pelos quais a original não navega. . . 58

Figura 42 – Semântica 3. . . 59

Figura 43 – Cálculo de b2 − 4ac compondo invocações de métodos. . . 63

Figura 44 – Cálculo de b2 − 4ac com funções assíncronas. . . 64

Figura 45 – Estratégia de Afluentes para combinar as vantagens de funções síncronas e assíncronas. . . 65

Figura 46 – Interfaces ISyncFn0, ISyncFn1 e ISyncFn2. . . 66

Figura 47 – Cálculo de b2 − 4ac compondo invocações de funções síncronas. . . 66

Figura 48 – Interface ICallback. . . 67

Figura 49 – Interfaces IAsyncFn0, IAsyncFn1 e IAsyncFn2. . . 67

Figura 50 – Funções sub e mul definidas com as interfaces IAsyncFn2. . . 67

Figura 51 – Interface IEval. . . 68

Figura 52 – Interfaces IEval0 e IEval1. . . 68

(12)

cronas. . . 69

Figura 55 – Processo de avaliação de b2− 4ac. . . 69

Figura 56 – Cálculo do somatório dos preços dos produtos de uma lista compondo invocações de funções síncronas. . . 71

Figura 57 – Cálculo do somatório dos preços dos produtos de uma lista compondo avaliadores de funções assíncronas e de funções sobre listas. . . 72

Figura 58 – Interface IEvalHolder. . . 72

Figura 59 – Proxy que implementa a interface IEvalHolder. . . 73

Figura 60 – Interface IPrefetcher. . . 73

Figura 61 – Trecho de código Java que usa o interpretador de Litoral. . . 74

Figura 62 – Gramática de Litoral na metalinguagem do ANTLR. . . 75

Figura 63 – Especificação Litoral cuja árvore sintática e AST são exibidas nas figuras 64a e 64b, respectivamente. . . 76

Figura 64 – Árvore sintática e AST da especificação da Figura 63 . . . 77

Figura 65 – Tabela de símbolos da especificação Litoral da Figura 63. . . 78

Figura 66 – Interpretação de especificações Litoral . . . 80

Figura 66 – Interpretação de especificações Litoral - Continuação . . . 81

Figura 66 – Interpretação de especificações Litoral - Continuação . . . 82

Figura 67 – Especificações Litoral que compõem o benchmark Emeio. . . 86

Figura 68 – Tabelas adicionadas ao banco de dados para otimizar a implementação de Emeio baseada no Hibernate. . . 87

Figura 69 – Índices criados no banco de dados de Emeio. . . 87

Figura 70 – Resultado das otimizações da implementação de Emeio baseada no Hibernate. . . 88

Figura 70 – Resultado das otimizações da implementação de Emeio baseada no Hibernate - continuação. . . 89

Figura 71 – Tempo total de execução das implementações de Emeio baseadas no interpretador de Litoral, Hibernate e EclipseLink JPA. . . 91

Figura 72 – Índices de OO7. . . 93

Figura 73 – Especificações Litoral que fazem parte de OO7. . . 93

Figura 74 – Resultados da otimização da implementação de OO7 baseada no Hi-bernate. . . 94

Figura 75 – Tempo total de execução das implementações de Emeio baseadas no interpretador de Litoral, Hibernate e EclipseLink JPA. . . 95

Figura 76 – Especificação Program Summary que declara navegações das mensagens enviadas por um usuário para seus remetentes e destinatários. . . 96

Figura 77 – Tabelas de Emeio. . . 111

(13)

Lista de abreviaturas e siglas

API Application Programming Interface

AST Abstract Syntax Tree BCB Banco Central do Brasil CAD Computer Aided Design

CAM Computer Aided Manufacturing CASE Computer Aided Software Engineering DSL Domain-Specific Language

EBNF Extended Backus-Naur Form E/S Entrada/Saída

JPA Java Persistence Architecture JPQL Java Persistence Query Language ORM Object-Relational Mapper

OODBMS Object-Oriented Database Management System REST Representational State Transfer

RMI Remote Method Invocation

SGBD Sistema de Gerenciamento de Banco de Dados SOAP Simple Object Access Protocol

SQL Structured Query Language VLSI Very Large Scale Integration XPath XML Path Language

(14)

Sumário

1 INTRODUÇÃO . . . 16 1.1 Objetivo . . . 16 1.2 ORMs . . . 16 1.3 Hipótese . . . 17 1.4 Estratégia . . . 18 1.4.1 Viabilidade . . . 19 1.4.2 Generalidade . . . 19 1.5 Avaliação . . . 20 1.6 Organização . . . 20 2 CONCEITOS BÁSICOS . . . 22

2.1 Sistemas Intensivos em Processamento e em E/S . . . 22

2.2 Otimização de Sistemas Intensivos em E/S . . . 23

2.2.1 Caching . . . 23

2.2.2 Agregação . . . 23

2.2.3 Carregamento sob Demanda e Pré-carregamento . . . 23

2.2.3.1 Pré-carregamento Informado e Especulativo . . . 24

2.2.4 Compressão . . . 24

2.2.5 Concorrência . . . 24

2.3 Carregamento sob Demanda, Pré-carregamento e Agregação em ORMs . . . 25

2.3.1 Tipos de Atributo . . . 25

2.3.2 Carregamento de Atributos do Tipo I . . . 27

2.3.3 Carregamento de Atributos do Tipo II que Referenciam Objetos . . . 27

2.3.4 Carregamento de Atributos do Tipo II que Referenciam Coleções . . . 28

2.3.5 Pré-carregamento Informado . . . 28 2.3.6 Pré-carregamento Especulativo . . . 30 2.4 Hipótese . . . 30 3 LITORAL . . . 32 3.1 Requisitos . . . 32 3.2 Sintaxe . . . 34 3.3 Semântica Informal . . . 35

3.3.1 Navegando Através de um Único Atributo . . . 36

3.3.2 Navegando Através de mais de um Atributo . . . 36

(15)

3.3.5 Travessias Recursivas . . . 39

3.4 Interpretador Definidor . . . 41

3.4.1 Representação de Classes e Objetos . . . 42

3.4.2 Representação de Especificações Litoral . . . 43

3.4.3 Análise Semântica . . . 43 3.4.4 Interpretação . . . 50 3.4.4.1 Argumentos S, filter e s . . . 53 3.4.4.2 Semântica 1 . . . 54 3.4.4.3 Semântica 2 . . . 56 3.4.4.4 Semântica 3 . . . 58

3.4.4.5 Escolha de uma Semântica . . . 60

4 INTERPRETADOR . . . 61

4.1 Afluentes . . . 61

4.1.1 Modelo de Programação . . . 62

4.1.2 Funções Síncronas . . . 62

4.1.3 Funções Assíncronas . . . 63

4.1.4 Avaliadores, Avaliações e Processo de Avaliação . . . 65

4.1.5 Representando Funções Síncronas . . . 65

4.1.6 Callbacks . . . 66

4.1.7 Representando Funções Assíncronas . . . 67

4.1.8 Avaliações . . . 67

4.1.9 Avaliadores . . . 68

4.1.10 Funções sobre Listas . . . 70

4.1.11 Avaliadores de Funções sobre Listas . . . 70

4.2 Projeto . . . 71

4.3 Implementação . . . 74

4.3.1 Análise Léxica e Sintática . . . 74

4.3.2 Análise Semântica . . . 76

4.3.3 Interpretação . . . 78

4.3.3.1 Nós que Atuam Sobre Objetos . . . 78

4.3.3.2 Nós que Atuam Sobre Listas . . . 79

5 AVALIAÇÃO . . . 83

5.1 Emeio . . . 84

5.1.1 Implementação Baseada no Hibernate . . . 85

5.1.2 Implementação Baseada no EclipseLink JPA . . . 89

5.1.3 Implementação Baseada no Interpretador de Litoral . . . 89

(16)

5.2.1 Implementação Baseada no Hibernate . . . 93

5.2.2 Implementação Baseada no EclipseLink JPA . . . 94

5.2.3 Implementação Baseada no Interpretador de Litoral . . . 94

5.2.4 Resultados . . . 94

6 TRABALHOS RELACIONADOS . . . 96

6.1 Program Summaries . . . 96

6.2 Scalpel . . . 98

6.3 AutoFetch . . . 99

6.4 Outros Trabalhos sobre Pré-carregamento Especulativo . . . 100

7 CONCLUSÃO . . . 101 7.1 Contribuições . . . 101 7.2 Trabalhos Futuros . . . 102 Referências . . . 103 APÊNDICE A – EMEIO . . . 109 A.1 Classes . . . 109 A.2 Tabelas . . . 111 APÊNDICE B – OO7 . . . 112 B.1 Classes . . . 112 B.2 Tabelas . . . 114 APÊNDICE C – DEMONSTRAÇÕES . . . 115 C.1 Seq . . . 115 C.2 NatSeq . . . 115 C.3 ListSeq . . . 118 C.4 Lim . . . 120 C.5 ListOptionSeq . . . 121 C.6 Sublist . . . 125 C.7 LitoralFacts . . . 135 C.8 AnalyzeSpecFacts . . . 143 C.9 InterpretSpecFacts . . . 151

(17)

1 Introdução

Sistemas computacionais são pervasivos em nossa sociedade. Em boa parte, isto se deve à maior eficácia e eficiência observadas quando a sociedade executa diferentes processos de negócio apoiada por tais sistemas. No Brasil, por exemplo, o uso da computação permite que eleitores conheçam os resultados de eleições apenas algumas horas depois de encerradas as votações. Antes das urnas eletrônicas, a apuração de votos durava vários dias. Além disto, como votos eram computados manualmente, esta etapa do processo eleitoral estava mais sujeita a erros e fraudes. A otimização de sistemas computacionais, portanto, contribui, em última análise, para uma sociedade mais eficaz e eficiente.

Muitos sistemas sobre os quais a sociedade se apóia são construídos em linguagens orientadas a objetos e manipulam dados armazenados em bancos de dados relacionais através de ORMs (Object-Relational Mappers). No BCB (Banco Central do Brasil), por exemplo, existe a diretriz de que todos os sistemas devem ser desenvolvidos em linguagens orientadas a objetos, devem armazenar dados em bancos relacionais e, por fim, devem interagir com estes bancos de dados através de ORMs. Esta diretriz se aplica não só aos sistemas corporativos (utilizados por mais de um departamento do BCB) como também aos departamentais (utilizados por um único departamento), independentemente destes sistemas serem desenvolvidos pelo próprio BCB ou contratados a terceiros.

1.1 Objetivo

Dada a sua importância para a sociedade, esta pesquisa visa aumentar o desempenho de sistemas baseados em ORMs.

1.2 ORMs

ORMs são populares porque eles reduzem o esforço de desenvolvimento de camadas de acesso a dados ao permitir, entre outras coisas, que sistemas manipulem objetos transientes e persistentes de maneira similar. Em particular, ORMs permitem que sistemas naveguem por objetos de ambos os tipos exatamente da mesma maneira. Por exemplo, o trecho de código Java (GOSLING et al., 2014) da Figura 1 navega de mensagens para seus remetentes (linha 4) e destinatários (linha 5), não importando se estes objetos existem apenas na memória primária (ou seja, são transientes) ou se um banco de dados relacional os armazena (ou seja, são persistentes). Infelizmente, este trecho de código é muito mais lento no segundo caso porque, para que ele navegue de uma mensagem para um objeto destino, ORMs precisam primeiro carregar o objeto destino do banco de dados.

(18)

1 List <Message> messages = getMessages ( ) ;

2 for ( Message message : messages ) {

3 printMessage ( message ) ;

4 printUser ( message . getSender ( ) ) ;

5 for ( User r e c i p i e n t : message . g e t R e c i p i e n t s ( ) ) { 6 printUser ( r e c i p i e n t ) ;

7 }

8 }

Figura 1 – Trecho de código Java que navega por objetos.

ORMs atenuam este problema pré-carregando (prefetching) objetos, ou seja, carre-gando objetos antes de um sistema navegar por eles. O pré-carregamento propriamente dito não reduz o tempo tc que ORMs levam para carregar objetos, mas dá a ORMs a

chance de pré-carregarem vários objetos executando uma única consulta SQL (Structured Query Language) (EISENBERG et al., 2004). Pré-carregar os mesmos objetos executando menos consultas é o que realmente reduz tc, pois isto diminui a incidência das latências

dos canais de comunicação entre ORMs e bancos de dados sobre tc (BERNSTEIN; PAL;

SHUTT, 2000).

Voltando ao exemplo da Figura 1: sem pré-carregamento, ORMs executam duas consultas para cada execução das linhas 4–5. A primeira carrega o remetente de uma mensagem sob demanda. A segunda carrega seus destinatários sob demanda. Portanto, se o método getMessages retorna n mensagens, então ORMs executam 2n consultas. Por outro lado, com pré-carregamento, ORMs podem pré-carregar remetentes e destinatários de todas as mensagens executando apenas 2 consultas quando a linha 1 é executada, não importando quantas mensagens getMessages retorna, reduzindo assim tc significativamente.

Como as consultas que pré-carregam remetentes e destinatários são independentes, elas podem ser executadas concorrentemente. Entretanto, a maioria dos ORMs executa consultas apenas sequencialmente.

1.3 Hipótese

Esta pesquisa parte da hipótese de que o pré-carregamento, em muitos casos, também dá a ORMs a chance de executar consultas concorrentemente e que, combinando esta otimização com consultas que carregam vários objetos, ORMs podem reduzir tc ainda

(19)

1 [ sender r e c i p i e n t s ]

Figura 2 – Especificação Litoral que declara navegações de mensagens para seus reme-tentes e destinatários.

1 List <Message> messages = getMessages ( ) ; 2

3 String spec = " [ sender r e c i p i e n t s ] " ; 4 IP re fet c he r <List <Message>> p r e f e t c h e r =

5 new PrefetcherImpl <List <Message>>(spec ) {}; 6 p r e f e t c h e r . p r e f e t c h ( messages ) ;

7

8 for ( Message message : messages ) { /∗ . . . ∗/ }

Figura 3 – Trecho de código Java que pré-carrega objetos com o interpretador de Litoral.

1.4 Estratégia

Esta pesquisa define uma DSL (Domain-Specific Language) (DEURSEN; KLINT; VISSER, 2000) de especificação de navegações por objetos chamada Litoral. A Figura 2, por exemplo, exibe uma especificação Litoral que declara navegações de mensagens para seus remetentes e destinatários.

Também faz parte desta pesquisa o projeto e a implementação de um interpretador que executa especificações Litoral. O interpretador navega por objetos transientes e persistentes e pré-carrega os do segundo tipo executando consultas concorrentemente. O interpretador foi projetado de forma a absorver todas as questões relativas à concorrência, delegando a ORMs questões relativas à interação com bancos de dados. Graças a esta divisão de responsabilidades, qualquer ORM pode embutir o interpretador e assim pré-carregar objetos executando consultas concorrentemente. A Figura 3, por exemplo, exibe o trecho de código previamente exibido na Figura 1 modificado para usar o interpretador.

A linha 4 instancia o interpretador a partir da especificação Litoral contida na linha 3. A linha 5 executa o interpretador passando-lhe como parâmetro uma lista de mensagens. Quando a execução da linha 5 chega ao fim, o interpretador terá pré-carregado os remetentes e destinatários das mensagens. A grande diferença entre os trechos de código exibidos nas Figuras 1 e 3 é que no primeiro, que não faz uso do interpretador, ORMs pré-carregam objetos executando consultas sequencialmente, enquanto que no segundo o interpretador pré-carrega objetos executando consultas concorrentemente.

Um aspecto positivo desta estratégia é que ela permite que programadores desen-volvam sistemas que pré-carregam objetos executando consultas concorrentemente sem, contudo, lidar com programação concorrente, reconhecidamente difícil (NANZ; WEST; SILVEIRA, 2013).

(20)

Outro aspecto que merece destaque nesta estratégia é que ela pode ser adotada pontualmente e também a posteriori. O trecho de código da Figura 3, por exemplo, é funcionalmente idêntico com ou sem o interpretador. A diferença nestes dois casos é apenas não-funcional. Com o interpretador, o trecho de código é executado mais rapidamente. Portanto, pode-se identificar em códigos preexistentes os pontos que oferecem maiores oportunidades para pré-carregar objetos executando consultas concorrentemente e adotar o interpretador apenas nestes pontos.

1.4.1 Viabilidade

Existe um debate em torno da viabilidade de programadores especificarem navega-ções. De um lado do debate, (IBRAHIM; COOK, 2006) argumenta que é difícil especificar navegações corretamente. Adicionalmente, manutenções evolutivas e corretivas podem invalidar especificações previamente válidas. Estes trabalhos propõem métodos que permi-tam a ORMs especular quais navegações sistemas execupermi-tam, eliminando a necessidade de programadores especificarem navegações.

Do outro lado do debate, (GUÉHIS; GOASDOUÉ-THION; RIGAUX, 2009) defende que programadores especialistas podem especificar navegações com relativa facilidade e que o aumento de desempenho obtido a partir delas compensa seus custos. Uma evidência de que esta opinião é no mínimo razoável é que ORMs bastante populares (BLAKE-LEY et al., 2006; ADYA et al., 2007; CASTRO; MELNIK; ADYA, 2007; LINSKEY; PRUD’HOMMEAUX, 2007; O’NEIL, 2008) oferecem mecanismos para que programadores especifiquem navegações. Além disto, como os métodos de especulação são falíveis, eles podem resultar no pré-carregamento de mais ou menos objetos do que o necessário, preju-dicando o desempenho dos sistemas que os adotam. Portanto, pelo menos nestes casos, é preciso um mecanismo que permita ao programador sobrepor as especulações dos métodos.

Apesar desta pesquisa se alinhar aos trabalhos que defendem que os benefícios superam os custos, ela continuará válida mesmo que esta hipótese não se confirme. Isto acontece porque a otimização que ela propõe, ou seja, pré-carregar objetos executando consultas concorrentemente, pode ser implementada a partir de navegações especificadas automaticamente ou manualmente.

1.4.2 Generalidade

O projeto do interpretador é genérico o bastante para que ele possa delegar a interação com o repositório de objetos não só a um ORM (caso os objetos estejam armazenados em um banco de dados relacional), mas também a qualquer outro mecanismo de acesso a objetos, independentemente de qual é o repositório subjacente. É possível, por exemplo, usar o interpretador para pré-carregar concorrentemente objetos que estão

(21)

armazenados em um outro sistema. Basta que o último adote algum mecanismo de computação distribuída tal como RMI (WALDO, 1998), SOAP (RYMAN, 2001), REST (FIELDING; TAYLOR, 2002), etc. É por conta desta generalidade que a presente pesquisa é intitulada “Otimizando Sistemas Intensivos em Entrada/Saída Através de Programação Concorrente”.

1.5 Avaliação

A estratégia da Seção 1.4 foi avaliada com os benchmarks sintéticos (CURNOW; WICHMANN, 1976) Emeio, desenvolvido no contexto desta pesquisa, e OO7 (CAREY; DEWITT; NAUGHTON, 1993; CAREY et al., 1994). No primeiro, pré-carregar objetos executando consultas concorrentemente aumentou a velocidade de execução em até 323,6%. No segundo, o aumento foi de até 245,7%.

Os benchmarks também foram implementados com os ORMs Hibernate (O’NEIL, 2008) e EcliseLink JPA (KEITH; SCHINCARIOL, 2009), os quais aderem à especificação JPA (Java Persistence Architecture) (GROUP, 2013). O primeiro foi escolhido por ser bastante popular. O segundo foi escolhido por ser a implementação de referência desta especificação (FOUNDATION, 2008). As implementações baseadas no Hibernate e Eclip-seLink JPA foram significativamente otimizadas. Entretanto, em todos os cenários de Emeio e OO7 que oferecem oportunidades para pré-carregar objetos executando consul-tas concorrentemente, o desempenho delas foi inferior ao da implementação baseada no interpretador de Litoral.

1.6 Organização

O restante desta dissertação está organizado nos seguintes capítulos:

• o Capítulo 2 discute conceitos básicos para o entendimento desta pesquisa e, a partir deles, justifica a hipótese da Seção 1.3;

• o Capítulo 3 apresenta a sintaxe de Litoral em notação EBNF (Extended Backus-Naur Form) e sua semântica através de exemplos e de um interpretador definidor (definitional interpreter) (REYNOLDS, 1972) desenvolvido com o assistente de provas (GEUVERS, 2009) Coq (BERTOT; CASTÉRAN, 2004);

• o Capítulo 4 apresenta o projeto e a implementação do interpretador de Lito-ral, o qual foi construído sobre o framework Afluentes (ARAUJO et al., 2014), desenvolvido no contexto desta pesquisa e também apresentado neste capítulo;

(22)

• o Capítulo 5 apresenta os bechmarks Emeio e OO7 e as implementações destes benchmarks baseadas no Hibernate, EclipseLink JPA e no interpretador de Litoral, comparando o desempenho destas implementações;

• o Capítulo 6 discute trabalhos relacionados a esta pesquisa;

• o Capítulo 7 conclui esta dissertação elencando suas contribuições e possíveis traba-lhos futuros.

(23)

2 Conceitos Básicos

Este capítulo discute conceitos básicos para o entendimento da presente pesquisa e, a partir deles, justifica a hipótese da Seção 1.3.

2.1 Sistemas Intensivos em Processamento e em E/S

O tempo total tt que um sistema computacional leva para executar uma tarefa é

formado por duas parcelas:

1. o tempo te/sque o processador está ocioso, esperando que operações de E/S transfiram

dados para a memória primária e secundária, respectivamente. No caso de sistemas baseados em ORMs, esta parcela incorpora o tempo de carregamento de objetos tc,

discutido na seção 1.2;

2. o tempo tp que o processador está ocupado, processando os dados transferidos para

a memória primária.

A caracterização de te/s apresentada acima é a usualmente encontrada em livros de

sistemas operacionais (TANENBAUM, 2008). Provavelmente ela é reminiscente de um tempo em que operações de E/S eram realizadas apenas para transferir dados entre a memória primária e meios de armazenamento de massa tais como fitas e discos magnéticos. Atualmente, operações de E/S são realizadas também para outros propósitos, tais como comunicação entre processos e em redes de computadores. Portanto, parece mais adequado chamar esta parcela de tempo de comunicação, seja esta comunicação com uma controladora de fita, de disco, com outro processo ou com outro computador. Esta pesquisa atribui a

te/s esta última caracterização, mais ampla.

Em muitos sistemas, uma destas parcelas domina tt. Os sistemas nos quais te/s é

dominante, como os que processam dados comerciais, são chamados de intensivos em E/S. Já os sistemas nos quais tp é dominante, como os que realizam cálculos científicos, são

chamados de intensivos em processamento.

Dada a natureza dos sistemas intensivos em processamento, só é possível aumentar significativamente o seu desempenho reduzindo tp. Nestes sistemas, reduções em te/s têm

baixo impacto sobre tt. Por exemplo, suponha que em um sistema te/s e tp respondam por

20% e 80% de tt, respectivamente. Neste caso, uma redução de 50% em te/s reduzirá tt em

(24)

Nos sistemas intensivos em E/S, os papéis se invertem. Reduções em tp têm baixo

impacto sobre tt. Portanto, nestes sistemas, reduzir te/s é a melhor estratégia quando se

deseja reduzir tt.

2.2 Otimização de Sistemas Intensivos em E/S

Esta seção discute estratégias para aumentar o desempenho de sistemas intensivos em E/S.

2.2.1 Caching

Caching consiste em armazenar na memória primária o resultado das operações de entrada que um sistema executa repetidas vezes. Desta forma, quando um sistema precisa, pela primeira vez, de um dado que não está na memória primária, uma operação de entrada é executada. Quando o sistema precisar novamente do mesmo dado, ele já se encontrará na memória primária, não sendo necessário efetuar uma operação de entrada novamente.

2.2.2 Agregação

Operações de E/S exibem uma latência significativa. Ou seja, o intervalo de tempo transcorrido entre o início e o fim de uma operação de E/S não é desprezível, mesmo quando o volume de dados transferidos é o menor possível (um bloco de um disco, por exemplo). Agregando diversas operações de E/S em uma única operação que produz os mesmos efeitos das operações agregadas, a latência das operações de E/S incide sobre um sistema uma única vez ao invés de diversas vezes.

2.2.3 Carregamento sob Demanda e Pré-carregamento

Dados podem ser carregados sob demanda ou pré-carregados. No primeiro caso, eles são carregados imediatamente antes do instante t em que um sistema os manipula. No segundo caso, eles são carregados com alguma antecedência a t.

O pré-carregamento propriamente dito não é uma otimização. Entretanto, por ser necessário à agregação de operações de entrada, ele é muitas vezes confundido com a última, esta sim uma otimização. Tal confusão é lamentável, pois, apesar de necessário, o pré-carregamento não é suficiente para a agregação, já que é possível pré-carregar dados executando operações de entrada que não foram agregadas.

(25)

2.2.3.1 Pré-carregamento Informado e Especulativo

O pré-carregamento pode ser informado ou especulativo. No primeiro caso, é necessário que programadores informem quais dados devem ser pré-carregados. O segundo caso elimina esta necessidade empregando métodos que especulam quais dados sistemas manipulam. Os métodos se baseiam em informações coletadas estática ou dinamicamente. De maneira geral, quanto mais próximas do sistema forem as informações coletadas, mais corretas são as especulações dos métodos.

Como os métodos de especulação são falíveis, o pré-carregamento baseado neles pode carregar menos ou mais dados do que o necessário, reduzindo o desempenho de sistemas. Um método que costuma falhar por excesso, por exemplo, consiste em supor que sistemas sempre manipulam todos os dados associados a um dado inicial. Graças a métodos imprecisos como este, o carregamento sob demanda é visto como uma otimização, pois ele nunca carrega mais dados do que o necessário. É importante ressaltar, entretanto, que o carregamento sob demanda nunca é mais rápido do que o pré-carregamento executado a partir de informações ou especulações corretas e costuma ser significativamente mais lento do que este pré-carregamento combinado com a agregação de operações de entrada.

2.2.4 Compressão

O tempo de execução de operações de E/S é uma função monotônica não-decrescente do volume de dados transferidos. Portanto, comprimindo dados antes de transferi-los, reduz-se potencialmente o tempo de execução de operações de E/S. É necessário descomprimir os dados após transferi-los, o que aumenta tp. Este aumento, entretanto, não costuma ser

significativo frente à redução no tempo de execução das operações de E/S, fazendo com que o saldo final desta estratégia seja positivo.

2.2.5 Concorrência

Caching, agregação e compressão reduzem te/s otimizando as operações de E/S que

um sistema executa. Uma estratégia ortogonal às anteriores para reduzir te/s consiste em

executar estas operações concorrentemente, sem atuar sobre seu desempenho. Por exemplo, suponha que um sistema executa duas operações independentes cujos tempos de execução são t1e t2, respectivamente. Se o sistema executa estas operações sequencialmente, então te/s

será a soma de t1 e t2. Entretanto, se o sistema executa estas operações concorrentemente, então te/s vai variar do máximo entre t1 e t2 (melhor caso) à soma de t1 e t2 (pior caso). O

melhor caso ocorrerá se existirem os recursos necessários à execução paralela das operações. No outro extremo, se só existirem recursos suficientes para executar uma operação de cada vez, ocorrerá o pior caso.

(26)

deve satisfazer para que a execução concorrente de operações de E/S seja uma otimização eficaz:

1. o sistema executa um número significativo de operações independentes; 2. os tempos de execução das operações são similares;

3. existem recursos computacionais suficientes para executar as operações em paralelo. Se a primeira condição é falsa, então existem poucas oportunidades para executar operações de E/S em paralelo. Se a segunda condição não é verdadeira, então a diferença entre o tempo que o processador espera pela execução sequencial e paralela das operações não é significativo. Por fim, se a terceira condição não é satisfeita, então as operações são executadas sequencialmente mesmo quando o sistema as requisita concorrentemente.

2.3 Carregamento sob Demanda, Pré-carregamento e Agregação

em ORMs

Sistemas requisitam objetos a ORMs especificando seus identificadores, ou seja, valores que os identificam unicamente, ou predicados que eles devem satisfazer. O trecho de código Java da Figura 4 exemplifica o segundo tipo de requisição. A linha 1 contém uma consulta JPQL (Java Persistence Query Language) (GROUP, 2013) que especifica as mensagens enviadas pelo usuário cujo identificador é igual a 1. As linhas 2–3 requisitam estas mensagens a ORMs compatíveis com a especificação JPA. As seções A.1 e A.2 contêm o código das classes que este trecho de código manipula e as tabelas que armazenam seus objetos, respectivamente.

2.3.1 Tipos de Atributo

Objetos possuem dois tipos de atributo. O tipo I armazena informações acerca do objeto propriamente dito. O nome de um usuário e a data de envio de uma mensagem são exemplos deste tipo. O tipo II representa associações entre objetos. Exemplos deste último tipo são o remetente, os destinatários e os anexos de uma mensagem.

Na maioria dos casos, sistemas requisitam objetos sem especificar quais atributos devem ser carregados. Se ORMs carregam atributos sob demanda, então eles executam uma consulta SQL para carregar cada atributo de cada objeto que os sistemas manipulam. A Figura 5, por exemplo, exibe as consultas SQL que ORMs executam para carregar sob demanda os atributos de mensagens manipulados pelo trecho de código da Figura 4.

(27)

1 String j p q l = " s e l e c t m from Message m where m. sender . id = 1 " ; 2 TypedQuery<Message> query = jpa . createQuery ( jpql , Message . class ) ; 3 List <Message> messages = query . g e t R e s u l t L i s t ( ) ;

4

5 for ( Message message : messages ) {

6 p r i n t l n ( message . getDate ( ) ) ; 7 p r i n t l n ( message . getSubject ( ) ) ; 8

9 User sender = message . getSender ( ) ; 10 p r i n t l n ( sender . getName ( ) ) ;

11

12 List <User> r e c i p i e n t s = message . g e t R e c i p i e n t s ( ) ; 13 for ( User r e c i p i e n t : r e c i p i e n t s ) {

14 p r i n t l n ( r e c i p i e n t . name ) ;

15 }

16

17 List <File > f i l e = message . g e t F i l e s ( ) ; 18 p r i n t l n ( f i l e s . s i z e ( ) > 0 ) ;

19 }

Figura 4 – Trecho de código Java que requisita objetos especificando o predicado que eles devem satisfazer.

1 select sent_date from message where i d e n t i f i e r = /∗ . . . ∗/ 2

3 select s u b j e c t from message where i d e n t i f i e r = /∗ . . . ∗/ 4

5 select s e n d e r _ i d e n t i f i e r from message where i d e n t i f i e r = /∗ . . . ∗/ 6

7 select u s e r _ i d e n t i f i e r from message_recipients 8 where m e s s a g e _ i d e n t i f i e r = /∗ . . . ∗/

9

10 select f i l e _ i d e n t i f i e r from message_file 11 where m e s s a g e _ i d e n t i f i e r = /∗ . . . ∗/

Figura 5 – Consultas SQL que ORMs executam para carregar sob demanda os atributos de mensagens manipulados pelo trecho de código da Figura 4.

(28)

1 select ∗ from message where i d e n t i f i e r = /∗ . . . ∗/

Figura 6 – Consulta SQL que pré-carrega todos os atributos do primeiro tipo de uma mensagem.

1 select message . ∗ , sender .∗ from message

2 l e f t join user_table sender

3 on sender . i d e n t i f i e r = message . s e n d e r _ i d e n t i f i e r 4 where message . i d e n t i f i e r = /∗ . . . ∗/

Figura 7 – Consulta SQL que pré-carrega todos os atributos do tipo I de uma mensagem e, através de uma junção, também seu remetente.

Por outro lado, se ORMs pré-carregam especulativamente atributos do tipo I, então eles executam a consulta SQL exibida na Figura 6 ao invés das duas primeiras consultas SQL da Figura 5.

2.3.2 Carregamento de Atributos do Tipo I

É um tanto surpreendente, mas os tempos de execução t1, t2 e t3 das duas primeiras consultas SQL da Figura 5 e da consulta SQL da Figura 6, respectivamente, são similares. Isto acontece porque as latências dos canais de comunicação entre ORMs e bancos de dados dominam estes tempos. (BOWMAN; SALEM, 2007), por exemplo, relata que a latência corresponde a 95% ou mais do tempo de execução de consultas SQL que, como estas, selecionam uma única linha e podem ser respondidas com índices.

Portanto, no trecho de código da Figura 4, se ORMs carregam atributos sob demanda, então o tempo de carregamento dos atributos do tipo I de cada mensagem será igual a t1+t2 ≈ 2t3, enquanto que se eles pré-carregam atributos do tipo I especulativamente, este tempo será igual t3, ou seja, aproximadamente metade do primeiro. É por conta de reduções assim que ORMs pré-carregam atributos do tipo I especulativamente.

2.3.3 Carregamento de Atributos do Tipo II que Referenciam Objetos

ORMs precisam decidir como carregar os atributos tipo II. Uma estratégia consiste em também pré-carregá-los. A Figura 7, por exemplo, exibe a consulta SQL que ORMs executam para pré-carregar todos os atributos de uma mensagem do tipo I e, através de uma junção (AHO; BEERI; ULLMAN, 1979), também um atributo do tipo II: seu remetente.

Apesar de ser mais complexa do que a da Figura 6, a consulta SQL da Figura 7 também goza das propriedades que fazem com que sua execução seja dominada pela latência: ela seleciona uma única linha e pode ser respondida com índices. O remetente de

(29)

uma mensagem é um exemplo de atributo do tipo II que referencia um único objeto. De maneira geral, o tempo de execução de consultas SQL que pré-carregam somente atributos do tipo I é apenas um pouco menor do que o daquelas que também pré-carregam atributos do tipo II que referenciam objetos. É por conta disto que alguns ORMs, em particular

aqueles compatíveis com a especificação JPA, pré-carregam especulativamente atributos do tipo II que referenciam objetos.

2.3.4 Carregamento de Atributos do Tipo II que Referenciam Coleções

ORMs ainda precisam decidir como carregar os atributos do tipo II que referenciam coleções (listas, conjuntos, mapas, etc.) de objetos. Pré-carregá-los não é uma boa idéia pois, diferentemente dos casos anteriores, as consultas SQL que também pré-carregam estes atributos, na maioria dos casos, selecionam mais de uma linha e, quanto maior o número de linhas selecionadas, menor é a influência da latência no tempo de execução de consultas SQL. Colocando de outra forma: o tempo de execução das consultas SQL que pré-carregam atributos do tipo I e do tipo II que referenciam objetos costuma ser menor que a latência, enquanto que o tempo de execução daquelas que também pré-carregam atributos do tipo II que referenciam coleções costuma ser maior.

A consulta SQL da Figura 8, por exemplo, pré-carrega todos os atributos de uma mensagem. Sejam nr, na e nt o número de destinatários, anexos e tags da mensagem,

respectivamente. Então esta consulta seleciona max(1, nr)×max(1, na)×max(1, nt) linhas. ORMs carregam sob demanda atributos do tipo II que referenciam coleções injetando

proxies (GAMMA et al., 1994) nos atributos. Cada proxy carrega e armazena a coleção que ele representa quando um sistema executa qualquer método dele pela primeira vez. Em seguida, ele encaminha a execução do método à coleção. Quando o sistema executa qualquer método do proxy novamente, ele apenas encaminha a execução do método para a coleção. ORMs também usam proxies para carregar sob demanda atributos do tipo I ou do tipo II que referenciam objetos.

2.3.5 Pré-carregamento Informado

Carregando sob demanda e pré-carregando atributos tal como descrito até aqui, ORMs executam duas consultas a cada iteração do laço da Figura 4. A primeira carrega os destinatários de uma mensagem. A segunda seus anexos. Portanto, se o laço é executado

n vezes, então ORMs executam 2n consultas.

Entretanto, ORMs podem pré-carregar os remetentes e os anexos de todas as mensagens que o laço da Figura 4 manipula executando apenas as duas consultas que fazem uso do operador in de SQL exibidas na Figura 9.

(30)

1 select message . ∗ , sender . ∗ , r e c i p i e n t . ∗ , f i l e . ∗ , tag .∗ from message

2 l e f t join user_table sender

3 on sender . i d e n t i f i e r = message . s e n d e r _ i d e n t i f i e r 4 5 l e f t join message_recipient 6 on message_recipient . m e s s a g e _ i d e n t i f i e r = message . i d e n t i f i e r 7 l e f t join user_table r e c i p i e n t 8 on r e c i p i e n t . i d e n t i f i e r = message_recipient . r e c i p i e n t _ i d e n t i f i e r 9 10 l e f t join message_file 11 on message_file . m e s s a g e _ i d e n t i f i e r = message . i d e n t i f i e r 12 l e f t join f i l e 13 on f i l e . i d e n t i f i e r = message_file . f i l e _ i d e n t i f i e r 14 15 l e f t join message_tag 16 on message_tag . m e s s a g e _ i d e n t i f i e r = message . i d e n t i f i e r 17 l e f t join tag 18 on tag . i d e n t i f i e r = message_tag . t a g _ i d e n t i f i e r 19 where message . i d e n t i f i e r = /∗ . . . ∗/

Figura 8 – Consulta SQL que pré-carrega todos os atributos de uma mensagem.

1 select r e c i p i e n t . ∗ from message

2 l e f t join message_recipient 3 on message_recipient . m e s s a g e _ i d e n t i f i e r = message . i d e n t i f i e r 4 l e f t join user_table r e c i p i e n t 5 on r e c i p i e n t . i d e n t i f i e r = message_recipient . r e c i p i e n t _ i d e n t i f i e r 6 where message . i d e n t i f i e r in ( /∗ . . . ∗/ ) 7

8 select f i l e . ∗ from message

9 l e f t join message_file

10 on message_file . m e s s a g e _ i d e n t i f i e r = message . i d e n t i f i e r 11 l e f t join f i l e

12 on f i l e . i d e n t i f i e r = message_file . f i l e _ i d e n t i f i e r 13 where message . i d e n t i f i e r in ( /∗ . . . ∗/ )

Figura 9 – Consultas que pré-carregam os destinatários e os remetentes de várias mensagens através do operador in de SQL.

(31)

1 String j p q l = " s e l e c t m from Message m where m. sender . id = 1 " ; 2 TypedQuery<Message> query = jpa . createQuery ( jpql , Message . class ) ; 3

4 EntityGraph<Message> spec = manager . createEntityGraph ( Message . class ) ; 5 spec . addSubgraph ( " r e c i p i e n t s " ) ;

6 spec . addSubgraph ( " f i l e s " ) ;

7 query . setHint ( " javax . p e r s i s t e n c e . loadgraph " , spec ) ; 8

9 List <Message> messages = query . g e t R e s u l t L i s t ( ) ; 10

11 for ( Message message : messages ) { /∗ . . . ∗/ }

Figura 10 – Trecho de código Java que informa a ORMs compatíveis com a especificação JPA quais objetos eles devem pré-carregar.

A Figura 10 exemplifica o primeiro. Ela adiciona ao trecho de código da Figura 4 as linhas 4–7. Elas requisitam a ORMs compatíveis com a especificação JPA o pré-carregamento dos destinatários e anexos das mensagens resultantes da consulta JPQL da linha 1.

2.3.6 Pré-carregamento Especulativo

O laço da Figura 4 é bastante regular. Ele sempre navega de cada mensagem para seu remetente, destinatários e anexos. Regularidades como esta motivaram (BERNSTEIN; PAL; SHUTT, 2000) a propor um método de especulação que consiste em ORMs armazenarem o contexto em que eles carregaram ou pré-carregaram objetos.

O contexto de um objeto A é o conjunto de objetos que foi carregado ou pré-carregado pela operação (consulta, navegação, etc.) que carregou ou pré-carregou A. Quando um sistema navega de A para outro objeto B através de um atributo b, ORMs pré-carregam todos os objetos referenciados pelos objetos do contexto de A através do atributo b.

Aplicando este método no trecho de código da Figura 4, ORMs pré-carregam os destinatários e os anexos de todas as mensagens quando as linhas 12 e 17 são executadas, respectivamente, pela primeira vez.

2.4 Hipótese

A partir das discussões e exemplos das seções anteriores, esta seção justifica a hipótese de que o tempo que ORMs levam para pré-carregar objetos executando consultas concorrentemente é menor do que sequencialmente.

Sistemas baseados em ORMs adotam o estilo de programação exibido na Figura 4 (IBRAHIM; WIEDERMANN; COOK, 2009). Neste estilo, é comum que sistemas, exceto os

(32)

mais simples, naveguem através de dois ou mais atributos do tipo II. Nestes casos, tal como exemplificado na Subseção 2.3.5, as consultas que pré-carregam os objetos referenciados pelos atributos são independentes.

As consultas que ORMs executam para pré-carregar objetos referenciados por atributos do tipo II são bastante simples. Como elas são compostas apenas por restrições sobre chaves primárias e estrangeiras, seus tempos de execução costumam ser dominados pelas latências dos canais de comunicação entre ORMs e bancos de dados (BERNSTEIN; PAL; SHUTT, 2000). Isto faz com que estes tempos sejam similares.

Sistemas de gerenciamento de banco de dados são executados com frequência em instalações que possuem os recursos necessários para a execução paralela de diversas consultas. O surgimento e a popularização da computação em nuvem (ARMBRUST et al., 2010) têm contribuído para que isto ocorra cada vez mais.

Como ORMs executam consultas através de operações de E/S, as últimas gozam das propriedades das primeiras. Por exemplo, se duas consultas são independentes, então as operações de E/S através das quais elas são executadas também são independentes.

Conclui-se, portanto, que, em muitos casos, ORMs pré-carregam objetos executando operações de E/S independentes, cujos tempos de execução são similares e existem recursos computacionais suficientes para executar estas operações em paralelo. Ou seja, o pré-carregamento de objetos executado por ORMs satisfaz as condições discutidas na seção 2.2.5 para que a execução concorrente de operações de E/S seja uma otimização eficaz.

(33)

3 Litoral

Este capítulo apresenta Litoral, uma DSL para especificação de navegações por objetos.

3.1 Requisitos

A fase de análise (MERNIK; HEERING; SLOANE, 2005) do desenvolvimento de Litoral, determinou que a linguagem deveria atender os requisitos a seguir.

Embarque: deve ser possível embutir Litoral em linguagens orientadas a objetos que suportam reflexão. Mais especificamente, que permitem a sistemas, em tempo de execução, descobrir a classe de um objeto e obter o valor de um atributo a partir do seu nome.

Independência: Litoral deve ser independente de mecanismos de acesso a dados, ou seja, ela deve ser capaz de especificar navegações por objetos armazenados em bancos de dados (relacionais ou não), sistemas de arquivos, outros sistemas, etc.

Agregação: tal como ocorre em linguagens orientadas a coleções (SIPELSTEIN; BLELLOCH, 1991), as construções de Litoral devem operar sobre agregações de outros tipos de dados. Litoral satisfaz este requisito tratando objetos como coleções unitárias e oferecendo construções que operam apenas sobre coleções.

Recursão: Litoral deve ser capaz de especificar navegações iterativas e recursivas. O conceito de travessia, apresentado nas seções 3.3.4 e 3.3.5, cumpre esta demanda.

Concisão: especificações Litoral serão tipicamente armazenadas em variáveis ou passadas como parâmetros em linguagens orientadas a objetos, daí a importância delas ocuparem poucas linhas de código.

Segurança: erros do tipo null pointer (HOVEMEYER; SPACCO; PUGH, 2005) não devem ocorrer. Para atender este requisito, Litoral verifica se é possível navegar através de um atributo, ou seja, se ele referencia algum objeto. Em caso positivo, a navegação é executada, caso contrário ela é interrompida.

Declaração: Litoral deve ser uma linguagem declarativa, ou seja, ela deve oferecer construções que permitem especificar navegações sem entrar nos detalhes de como elas devem ser executadas.

Concorrência: especificações Litoral devem codificar tanto dependências entre navegações, ou seja, quando uma segunda navegação só pode ser executada após uma primeira, quanto as oportunidades para que elas sejam executadas concorrentemente.

(34)

1 List <Message> getMessages ( int userId ) {

2 String j p q l = " s e l e c t m from Message m where m. sender . id = " + userId ; 3 TypedQuery<Message> query = orm . createQuery ( jpql , Message . class ) ; 4 return query . g e t R e s u l t L i s t ( ) ;

5 }

Figura 11 – Método Java que carrega as mensagens enviadas por um usuário. 1 List <Message> getMessages ( int userId ) {

2 String j p q l = " s e l e c t m from Message m where m. sender . id = " + userId ; 3

4 EntityGraph<Message> spec = manager . createEntityGraph ( Message . class ) ; 5 spec . addSubgraph ( " r e c i p i e n t s " ) ;

6

7 TypedQuery<Message> query = orm . createQuery ( jpql , Message . class ) ; 8 query . setHint ( " javax . p e r s i s t e n c e . loadgraph " , spec ) ;

9 return query . g e t R e s u l t L i s t ( ) ;

10 }

Figura 12 – Método Java que carrega as mensagens enviadas por um usuário e pré-carrega seus remetentes.

Por independerem de mecanismos de acesso a dados, especificações Litoral podem ser facilmente incorporadas a qualquer camada (apresentação, negócio, acesso a dados, etc.) de um sistema. Para perceber porque isto é importante, suponha que o método getMessages exibido na Figura 11 pertença à camada de acesso a dados de um sistema. Suponha também que um desenvolvedor identificou que, após invocar getMessages, uma parte do sistema navega das mensagens retornadas por este método para seus remetentes. A partir desta constatação, o desenvolvedor decide modificar o método getMessages para que ele, além de carregar as mensagens enviadas por um usuário, também pré-carregue seus remetentes. Esta modificação pode ser vista nas linhas 4–5 e 8 da segunda versão do método getMessages exibida na Figura 12.

A segunda versão de getMessages aumenta o desempenho da parte do sistema que o desenvolvedor analisou. Isto acontece porque o ORM, fazendo uso de junções, carrega as mensagens e seus remetentes executando uma única consulta SQL enquanto que na primeira versão de getMessages, o ORM executa no mínimo duas consultas, uma para carregar as mensagens e outra para carregar seus remetentes. A primeira é executada imediatamente. A segunda é executada apenas quando o sistema navega para os remetentes.

Infelizmente, a segunda versão de getMessages reduz o desempenho de outras partes do sistema que também utilizam este método mas que não navegam das mensagens para seus remetentes. Nestes casos, a segunda versão de getMessages pré-carrega os reme-tentes desnecessariamente. Para evitar esta perda de desempenho, resta ao desenvolvedor

(35)

(a)

(b)

Figura 13 – Gramática de Litoral em notação EBNF e em diagramas de sintaxe. uma única alternativa, manter as duas versões de getMessages no sistema, utilizando cada uma delas quando for mais apropriado.

É fácil ver que existe uma forte tensão entre desempenho e reuso de código (IBRAHIM; COOK, 2006). O problema aqui é que a especificação de quais objetos devem ser carregados faz parte da API (Application Programming Interface) de um ORM e, portanto, está confinada às camadas de acesso a dados de sistemas, onde seu uso pode ter efeitos globais devido ao reuso de código. Especificações Litoral, por outro lado, podem ser incorporadas aos pontos mais externos de sistemas (por exemplo, nas camadas de apresentação), tendo, portanto, efeitos mais localizados.

3.2 Sintaxe

A figura 13 apresenta a gramática de Litoral em notação EBNF (Extended Backus–Naur Form) e também através de diagramas de sintaxe. Os símbolos s p e c i f i c a t i o n,

t r a v e r s a l, path e step são não-terminais. ID e os símbolos entre aspas simples são terminais. s p e c i f i c a t i o n é o símbolo inicial. A expressão regular [ a−zA−Z ] [ a−zA−Z0−9]∗ gera ID, ou seja, um ID é formado por uma letra seguida de outras letras ou números.

(36)

1 void printMessage ( Message message ) { 2 p r i n t l n ( message . getDate ( ) ) ;

3 p r i n t l n ( message . getSubject ( ) ) ; 4 p r i n t l n ( message . getBody ( ) ) ; 5

6 printUser ( message . getSender ( ) ) ; 7

8 for ( User r e c i p i e n t : message . g e t R e c i p i e n t s ( ) ) { 9 printUser ( r e c i p i e n t ) ;

10 }

11

12 for ( F i l e attachment : message . getAttachments ( ) ) {

13 p r i n t l n ( attachment . getName ( ) ) ; 14

15 printMediaType ( attachment . getMediaType ( ) ) ;

16 }

17

18 for ( Message re ply : message . g e t R e p l i e s ( ) ) { 19 printMessage ( re ply ) ;

20 }

21 }

22

23 void printUser ( User user ) {

24 p r i n t l n ( user . getName ( ) ) ; 25 26 p r i n t F i l e ( user . g e tP i c tu r e ( ) ) ; 27 } 28 29 void p r i n t F i l e ( F i l e f i l e ) { 30 p r i n t l n ( f i l e . getName ( ) ) ; 31 32 printMediaType ( f i l e . getMediaType ( ) ) ; 33 } 34

35 void printMediaType ( MediaType mediaType ) {

36 p r i n t l n ( mediaType . getType ( ) ) ; 37 p r i n t l n ( mediaType . getSubtype ( ) ) ;

38 }

Figura 14 – Trecho de código Java que navega por objetos.

3.3 Semântica Informal

Esta seção se inspira em (MEYEROVICH et al., 2009) e apresenta a semântica de Litoral informalmente através de vários exemplos que, progressivamente, especificam as navegações que o trecho de código Java exibido na Figura 14 executa.

(37)

1 sender

Figura 15 – Especificação Litoral que navega através de um único atributo. 1 s p e c i f i c a t i o n // símbolo i n i c i a l

2 t r a v e r s a l ∗ path // s u b s t i t u i ç ã o de s p e c i f i c a t i o n

3 path // t r a v e r s a l é opcional

4 step | [ step +] // s u b s t i t u i ç ã o de path

5 step // e s c o l h a do primeiro ramo

6 ID | ID . path | ID ( ) // s u b s t i t u i ç ã o de s t e p

7 ID // e s c o l h a do primeiro ramo

8 sender // ID gera sender

Figura 16 – Geração da especificação da Figura 15 a partir da gramática de Litoral. 1 r e c i p i e n t s

Figura 17 – Especificação Litoral que navega através de coleções de objetos.

3.3.1 Navegando Através de um Único Atributo

O método printMessage da Figura 14 recebe uma mensagem como parâmetro e, na linha 6, navega para seu remetente através do atributo sender. A Figura 15 exibe a especificação Litoral que declara esta navegação. Ela não faz nenhuma referência à mensagem, pois o objeto que dá início a navegações fica implícito nas especificações Litoral. Por um lado, esta característica dificulta a compreensão de especificações Litoral quando lidas independentemente do código onde elas serão embutidas. Por outro lado, entretanto, ela torna as especificações Litoral mais concisas.

A Figura 16 mostra como gerar esta especificação a partir da gramática de Litoral. Para Litoral é irrelevante se atributos referenciam objetos ou coleções de objetos. A sintaxe que especifica navegações através de atributos é a mesma em ambos os casos. A especificação Litoral da Figura 17, por exemplo, declara uma navegação através do atributo r e c i p i e n t s, que referencia uma coleção de objetos. Ela possui a mesma estrutura sintática que a especificação da Figura 15, a qual declara uma navegação através do atributo sender, que referencia um único objeto.

3.3.2 Navegando Através de mais de um Atributo

Nas linha 8 e 12, o método printMessage da Figura 14 também navega para destinatários e anexos de mensagens através dos atributos r e c i p i e n t s e attachments, respectivamente. A Figura 18 exibe a especificação Litoral que declara navegações de mensagens para seus remetentes, destinatários e anexos. Os caracteres [ (abre colchetes) e

(38)

1 [

2 sender 3 r e c i p i e n t s 4 attachments

5 ]

Figura 18 – Especificação Litoral que navega através de mais de um atributo.

1 path // t r a v e r s a l é opcional

2 step | [ step +] // s u b s t i t u i ç ã o de path 3 [ step +] // e s c o l h a do segundo ramo 4 [ step step step ] // s u b s t i t u i ç ã o de s t e p+ 5

6 [ // espaços em branco e c a r a c t e r e s

7 step // de nova l i n h a são ignorados

8 step

9 step

10 ]

11

12 [

13 sender // s t e p gera sender , r e c i p i e n t s 14 r e c i p i e n t s // e attachments

15 attachments

16 ]

Figura 19 – Geração da especificação da Figura 18 a partir da gramática de Litoral. 1 [ sender ]

Figura 20 – Especificação Litoral que navega por um único atributo.

] (fecha colchetes) delimitam lista de atributos de um objeto através dos quais navegações são executadas. Estes atributos são separados por espaços em branco ou caracteres de nova linha.

A Figura 19 mostra como gerar esta especificação a partir do símbolo não terminal path.

Pode-se omitir os delimitadores de listas formadas por um único atributo. Portanto, a especificação Litoral exibida na Figura 20 é semanticamente equivalente à exibida na Figura 15

3.3.3 Navegando Indiretamente

Até aqui, as especificações Litoral declararam navegações apenas para objetos referenciados diretamente por um objeto inicial. Litoral, entretanto, é expressiva o

(39)

1 [

2 sender 3 r e c i p i e n t s

4 attachments . mediaType

5 ]

Figura 21 – Especificação Litoral que navega indiretamente.

1 [

2 sender . p i c t u r e . mediaType 3 r e c i p i e n t s

4 attachments . mediaType

5 ]

Figura 22 – Especificação Litoral que encadeia navegações indiretas.

bastante para especificar navegações indiretas, ou seja, navegações de um objeto inicial para um objeto intermediário e, deste último, para um objeto final, e assim por diante. Por exemplo, na linha 15 da Figura 14, o método printMessage navega de um anexo de uma mensagem para o seu tipo. Neste exemplo, a mensagem, o anexo e o tipo atuam como o objeto inicial, intermediário e final, respectivamente. A linha 4 da especificação Litoral exibida na Figura 21 incorpora esta navegação indireta através do operador . (ponto).

Em navegações indiretas, é comum que um objeto intermediário funcione como o objeto inicial de uma nova navegação indireta. Navegações deste tipo são especificadas em Litoral encadeando aplicações do operador . (ponto). O método printMessage da Figura 14, por exemplo, navega de uma mensagem para seu remetente através do atributo sender, deste remetente para sua fotografia através do atributo p i c t u r e e desta fotografia para o tipo do arquivo que a armazena através do atributo mediaType. A linha 2 da especificação Litoral exibida na Figura 22 incorpora esta navegação encadeando o operador . (ponto).

Em navegações indiretas, é possível que o atributo que leva ao objeto intermediário não referencie nenhum objeto ou referencie uma coleção vazia. Isto aconteceria, por exemplo, se as navegações que a especificação da Figura 22 declara fossem realizadas a partir de uma mensagem sem remetente ou sem anexos. No primeiro caso, não seria possível navegar para a foto do remetente. No segundo caso, não seria possível navegar para o tipo dos arquivos anexados à mensagem. Na maioria das linguagens de programação, navegações indiretas através de atributos que não referenciam nenhum objeto provocam um erro em tempo de execução. Estes erros não acontecem em Litoral pois as navegações indiretas declaradas por uma especificação Litoral só serão realizadas quando possível, ou seja, quando existir o objeto intermediário e, caso ele seja uma coleção de objetos, apenas se não for uma coleção vazia.

(40)

1 [

2 sender . p i c t u r e . mediaType 3 r e c i p i e n t s . p i c t u r e . mediaType 4 attachments . mediaType

5 ]

Figura 23 – Especificação Litoral que replica uma navegação. 1 v i s i t U s e r = p i c t u r e . mediaType 2 [ 3 sender . v i s i t U s e r ( ) 4 r e c i p i e n t s . v i s i t U s e r ( ) 5 attachments . mediaType 6 ]

Figura 24 – Especificação Litoral que define uma travessia.

3.3.4 Travessias

O método printMessage da Figura 14 navega não só de uma mensagem até o tipo da fotografia do seu remetente, mas também até o tipo da fotografia de cada um dos destinatários desta mensagem. A linha 3 da especificação Litoral exibida na Figura 23 incorpora esta última navegação.

Apesar de correta, a especificação da Figura 23 possui uma replicação indesejável, pois mudanças no método printUser da Figura 14 precisarão ser refletidas nela duas vezes. Litoral permite que replicações deste tipo sejam eliminadas com travessias, que nada mais são do que caminhos nomeados. Uma vez definida, pode-se usar o nome de uma travessia para percorrer o seu caminho, iniciando a partir de diferentes objetos, quantas vezes for necessário. A linha 1 da Figura 24, por exemplo, define uma travessia chamada v i s i t U s e r cujo caminho é p i c t u r e . mediaType. As linhas 4 e 5 iniciam esta travessia a partir do remetente e dos destinatários de uma mensagem, respectivamente.

A Figura 25 mostra como gerar esta especificação a partir da gramática de Litoral.

3.3.5 Travessias Recursivas

Travessias podem iniciar outras travessias e, em particular, a si mesmas. Isto nos permite escrever a versão final da especificação Litoral que declara os objetos pelos quais o método printMessage da Figura 14 navega. Esta última versão, exibida na Figura 26, define nas linhas 1 e 3 as travessias v i s i t U s e r e v is it Me s sag e, respectivamente. A travessia visitMes sage, além de iniciar a travessia v i s i t U s e r nas linhas 4 e 5, inicia a si mesma na linha 8.

Referências

Outline

Documentos relacionados

(grifos nossos). b) Em observância ao princípio da impessoalidade, a Administração não pode atuar com vistas a prejudicar ou beneficiar pessoas determinadas, vez que é

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

libras ou pedagogia com especialização e proficiência em libras 40h 3 Imediato 0821FLET03 FLET Curso de Letras - Língua e Literatura Portuguesa. Estudos literários

Ninguém quer essa vida assim não Zambi.. Eu não quero as crianças

Notas Explicativas do Conselho de Administração às Demonstrações Contábeis Consolidadas Semestres Findos em 30 de Junho de 2001 e

No contexto em que a Arte é trabalhada como recurso didático-pedagógico na Educação Matemática (ZALESKI FILHO, 2013), pode-se conceber Performance matemática (PM) como

O software abrirá uma área de trabalho, para a montagem do sistema a ser simulado, cuja plataforma está baseada em um sistema de coordenadas cartesianas sistema a ser simulado,

O Convênio, que certamente trará benefícios mútuos na condução das atividades de supervisão em ambos os países, propiciará, sem dúvida, um incremento na troca