Segunda-feira, Agosto 31, 2009

Dualidade memória/dispositivo

Ainda pensando sobre linguagem de programação e sistema, e na possibilidade de se projetar uma API de dispositivos apenas com byte e address<>, me ocorreram os seguintes pensamentos sobre a relação entre os dispositivos e a memória.

A memória é uma sequência onde ocorrem símbolos, na máquina binária contemporânea esses símbolos são bits e a unidade de armazenagem prática aos programas é o byte.

Além disso, na máquina de von Neumann, toda a operação do programa ocorre sobre a memória, inclusive operação sobre dispositivos, que ocorrem ao programa na forma de intervalos especialmente posicionados na memória.

Programas no sistema Unix, por outro lado, vêem dispositivos na forma de arquivos, com o qual trocam dados; o dispositivo é uma origem de dados e um destino de dados. A API fundamental de dispositivos é read e write. A realização básica de um arquivo é o próprio arquivo de dados, ocorrendo em um sistema de arquivamento.

É claro que o Unix, aplicando sua filosofia de simplicidade, exibe tudo como arquivo, inclusive todos os dispositivos; programas podem trocar dados com qualquer dispositivo representável em /dev. De fato, existe /dev/mem que reflete como um dispositivo a própria memória.

Por fim, borrando a distinção entre o que o processo de usuário vê e o que o sistema vê, existem os mecanismo de mapeamento em memória, que representam dispositivos (entre outras coisas) como intervalos de memória ao processo.

É fácil observar que a memória, fisicamente, e o dispositivo, abstratamente, são elementos e conceitos que se substituem e se sobrepõe. A noção de character device no Unix parece nascer exatamente daqueles dispositivos cuja natureza é a de um intervalo de átomos, um conceito cuja necessidade surge após a decisão de representar tudo como arquivos.

A união de memória virtual e o aspecto memória de um dispositivo ocorre no mecanismo de mapeamento em memória; no projeto de uma API de I/O assíncrono, a necessidade de expor páginas da memória de um processo ao driver do dispositivo para DMA também ocorre.

Programas sintonizados à API de arquivos frequentemente desejam comunicar a subprogramas dados em memória, e então a necessidade de um pseudo-dispositivo cuja representação é memória surge.

Tudo isso implica que o projeto de uma API de dispositivos é o projeto de uma API de memória, e todo projeto de um abstract data type dispositivo implica o projeto de um abstract data type intervalo-de-memória.

Que um aspecto ofereça melhor desempenho a um programa é função das necessidades desse programa. Programas que lêem arquivos frequentemente mapeiam este arquivo na memória e o lêem como um intervalo de memória. Programas que computam na memória o digest SHA-1 de uma informação frequentemente operam um pseudo-dispositivo onde escrevem blocos fixos de dados iterativamente.

Fundamentalmente, eis a questão: um dispositivo deve ser convertível para um intervalo-de-memória e vice-versa. O desafio é projetar tal API segundo todos os critérios de boas práticas realisticamente em um sistema.

Mudando de assunto, eis o projeto 9.1 do Elements of Programming:

"Modern computing systems include highly optimized library procedures for copying memory; for example, memmove and memcpy, which use optimization techniques not discussed in this book. Study the procedures provided on your platform, determine the techniques they use (for example, loop unrolling and software pipelining), and design abstract procedures expressing as many of these techniques as possible. What type requirements and preconditions are necessary for each technique? What language extensions would allow a compiler full flexibility to carry out these optimizations?"

Terça-feira, Agosto 25, 2009

Máquina de Turing

Acho que atingi uma compreensão de como uma máquina de computação contemporânea cumpre os requisitos restritos da máquina universal de computação.

A máquina universal de computação é uma máquina que opera sobre símbolos ocorrendo um espaço unidimensional infinito. A máquina transforma seu estado de acordo com o símbolo atual; ela pode, por exemplo, avançar ou retroceder no espaço de símbolos, reescrever o símbolo em uma determinada posição, ou qualquer outra coisa.

A máquina se torna útil com a especificação do conjunto de símbolos representáveis e do significado de todos esses símbolos. Assim, um símbolo pode significar "avance três posições" e outro símbolo pode significa "substitua o próximo símbolo com o símbolo seguinte a esse, e vice-versa.

É claro que nenhuma máquina concreta terá acesso a um espaço infinito para símbolos; portanto, podemos falar sobre uma máquina universal restrita com espaço finito para símbolos.

Uma máquina universal binária é uma máquina cujos símbolos são 0 e 1. Uma tal máquina não pode significar muita coisa com apenas um símbolo, mas nada impede que significados especiais sejam atribuídos a sequências específicas de símbolos.

Assim, podemos falar sobre símbolos complexos, compostos por uma sequência de símbolos, que na máquina binária será uma sequência de 0s e 1s. A máquina binária possui portanto uma regra implícita: enquanto um símbolo complexo não for compreendido, ela avança para o próximo símbolo.

Quando um símbolo completo é compreendido, a máquina realiza a transformação especificada para aquele símbolo complexo.

É claro que, em uma máquina concreta, o número de símbolos complexos especificáveis é finito, já que os próprios símbolos complexos devem ser sequências finitas de símbolos fundamentais, e portanto existe uma combinação finita de tais símbolos complexos.

Mas um conjunto suficientemente grande pode representar todas as operações possíveis sobre uma memória sequencial de símbolos fundamentais, como: se o valor lógico do próximo símbolo complexo for verdade avance para a posição determinada pelo símbolo complexo seguinte a este, senão pelo símbolo complexo duas posições depois.

Isso é verdade porque, se podemos computar matemática fundamental, podemos computar qualquer coisa.

Sexta-feira, Agosto 21, 2009

Tipos de dados primitivos 2

Em C++, existe uma certa ambiguidade no significado dos tipos primitivos.

Tome char, por exemplo. char pode ser apenas char, pode ser signed char ou pode ser unsigned char. Mas char deve ser caractere, certo? Qual é o significado de um caractere ser signed ou unsigned? Nenhuma.

Existe, é claro, uma razão porque é possível especificar unsigned ou signed char; porque char é um valor inteiro de tamanho 1. De certa forma, char é o menor de todos os int, e foi projetado para ser usado como tal. Em uma máquina convencional, char é um inteiro de 8 bits. C99 na prática exige que char ocupe 8 bits.

Em C original, o ponteiro para char possuía a propriedade adicional de representar qualquer endereço válido de memória. Portanto, uma função que lê dados de um dispositivo qualquer para a memória aceitaria um parâmetro do tipo ponteiro para char significando o endereço de memória de um buffer.

É claro que isso provoca uma confusão com outro sigficado de ponteiro para char, a string de bytes terminada por NULL, ou NTBS. Por esse motivo, alguns programas e bibliotecas usam ponteiro para unsigned char para representar segmentos opacos de memória, como buffers de I/O.

Em C99, qualquer ponteiro é conversível para um ponteiro para algum char. Em C++ isso não é permitido. Em ambas as linguagens, qualquer ponteiro é conversível para um ponteiro para void.

Aritmética de ponteiro não é permitia para um ponteiro para void, infelizmente; programas que desejam atravessar a memória por qualquer razão devem usar ponteiros para char. Isso exige o uso de conversões por reinterpretação, como a notação de cast com parênteses, ou reinterpret_cast.

Existem aqui diversas necessidades diferentes. Uma delas é endereçar memória opaca, sem interpretação, e endereçá-la completamente. Outra é representar o menor valor inteiro possível na plataforma. Outra é representar um caractere. Outra é representar um segmento de texto.

Representar um caractere como o menor inteiro da plataforma é, hoje sabemos, uma ingenuidade, e existem diversas representações de caracteres que usam um número variado dessas unidades para caracteres, ou usam um número fixo de duas ou mais dessas unidades.

Assim, quando ocorre em um programa um caractere literalmente, como f, qual é a quantidade de armazenagem que o valor correspondente ocupa na memória? Depende da representação de caracteres usada no sistema alvo -- e naturalmente na unidade de memória da arquitetura alvo.

Eu não gosto dessa notação de signed e unsigned. Parece ter havido um problema original em que os ints tiveram de ser subitamente diferenciados entre inteiros e inteiros positivos. Não entendo como isso pode ter sido súbito, mas ainda não conheço bem a história da computação para saber como eram as máquinas quando C foi projetada. Me parece apenas que é um sistema de tipos obsoleto. As máquinas conhecem os naturais, ou inteiros positivos, representados em base 2 e conhece os inteiros representados em base 2 com complemento de dois.

Caracteres são um outro tipo de informação, distinto de números, apesar de logicamente relacionáveis -- a representação de um caractere é um índice em uma tabela.

Terça-feira, Agosto 18, 2009

Declarações, nomes e objetos

Uma linguagem como C++ é chamada declarativa porque o elemento central de sua sintaxe é a declaração.

Uma declaração é uma construção sintática que associa um nome a um sujeito -- ou um identificador e um objeto. O caso simples é a declaração de uma variável.

Creio que um objeto é um ente totalmente abstrato do discurso, ao contrário do que a literatura sugere. Certamente podemos falar sobre o objeto e descrever suas propriedades; mas na máquina concreta, o significado de um objeto transita entre seus três aspectos fundamentais: seu valor, sua armazenagem e sua localização.

O tipo de um objeto, por princípio, corresponde ao domínio de seus valores. Assim, podemos considerar os dois tipos primitivos fundamentais que sugeri anteriormente, byte e address. byte é um pseudo-tipo para "o que quer que seja, armazenável na memória". address é o tipo do valor "localização na memória de um valor do tipo T". address é o tipo do valor "localização na memória do que quer que seja".

Em C++, a função de "o que quer que seja" é cumprida por char; char é a unidade de memória na máquina abstrata C++. A função de "localização na memória do que quer que seja" é cumprida em parte por void*, para o qual podemos converter qualquer T*, e em parte por char*, que garantidamente pode endereçar qualquer localização válida na memória.

Valores do tipo byte são apenas copiáveis; é possível, no mínimo, reproduzir o padrão de bits de uma armazenagem para a outra.
Valores do tipo address são copiáveis; além disso, o domínio de address é equivalente ao domínio dos naturais, e suporta a mesma aritmética; por fim, existe a operação de-referência, que transforma um valor address em um valor T.

Com esses dois tipos podemos fazer muito pouco, mas podemos fazer algo; é possível escrever um programa capaz de copiar dados de um dispositivo para o outro apenas com byte e address. Seria também possível escrever programas capazes de rearranjos e outras operações similares.

Esses programas não poderiam ser muito sofisticados porque os únicos julgamentos possíveis seriam sobre localizações de objetos, e não seus valores. Seria possível, por exemplo, escrever um programa que inverte uma sequência de bytes, etc.

Segunda-feira, Agosto 17, 2009

Tipos de dados primitivos

Após uma longa considerção sobre o assunto, e bastante tempo livre durante as férias, cheguei a uma opinião sobre que conjunto de tipos de dados primitivos eu gostaria de ver em Pedro C++.

A idéia de que há signed e unsigned como qualificadores distintos do tipo de dados não me parece muito coerente; um signed integer e um unsigned integer são valores em domínios completamente distintos. De fato, um unsigned integer é um natural, e foi essa intuição que me levou eventualmente a compilar a seguinte lista:

  • byte
  • address
  • natural
  • integer
  • rational
  • real
  • complex
Com os tipos primitivos byte a address seria possível escrever um sistema capaz de transportar dados entre dispositivos. Um tal sistema, ou sub-sistema, não tem interesse na representação de um dado; isso percebe-se claramente nas APIs de I/O do Unix, cujo tipo de dado é void*. Este tipo, em Pedro C++, seria um address.

Para a computação matemática pura temos a seleção completa de domínios algébricos. natural, integer e real são verdadeiramente primitivos, enquanto rational e complex são derivados destes. Em uma máquina convencional, natural é representado diretamente em base 2, integer é representado em base 2 por complemento de dois, e real é representado na forma dada em IEEE 754. Uma linguagem rica permitira que esses tipos sejam genéricos no tamanho; natural<16> é um natural ocupando dois bytes, integer<64> é um inteiro ocupando 8 bytes.

Deve ser possível, com base em natural, implementar qualquer domínio homomórfico com os naturais, como por exemplo caracteres em um mapa de caracteres.

É claro que nada disso é remotamente suficiente para uma linguagem na prática; é preciso ainda falar sobre muitas coisas muito simples, como a representação e o tipo de valores representados literalmente no programa etc.

NOTA: não tenho certeza se "homomórfico" foi a palavra certa ali em cima.

Terça-feira, Agosto 11, 2009

Por que eu desejo escrever explicitamente sobre o ambiente de programação C++?
Por que existem documentos com frases assim:

To compile your programs, link with the C++CSP2 library. On GCC this can be achieved by using the "-lcppcsp2" option on the command-line.

Se eu tentar compilar o programa de exemplo deste documento seguindo essas instruções à risca ocorerrá o seguinte:

/usr/bin/ld: cannot find -lcppcsp2
collect2: ld returned 1 exit status

Por quê? Por que você não pode ingenuamente dizer quais bibliotecas você quer; o ambiente de ligação de programas com bibliotecas envolve a localização da biblioteca. Onde ela está?

Segunda-feira, Agosto 10, 2009

Concordar Dói

Estou lendo agora artigos sobre certas construções em máquinas programáveis chamadas "corotinas". O artigo que eu estou terminando de ler nesse exato momento insinua, cheira, pinta, sugere e diz explicitamente que "corotinas" são muito melhores que "subrotinas" por infinitas razões.

Compreender o que é "corotina" para esse autor me obriga a compreender como "corotina" é melhor que "subrotina"; talvez depois de compreender como isso é verdade para essa pessoa, eu saberia destilar meu próprio conhecimento.

É intrigante como esse concordar é revoltante. A leitura do texto é difícil por essa razão; eu não quero concordar com julgamentos sobre o valor do que eu estou aprendendo agora.

Porém, acho interessante como a idéia de concordar com uma idéia temporariamente, a título de argumentação, é tão difícil. A mente rejeita este estado de acordo, mesmo que breve, como se realizá-lo por um breve momento fosse já uma desestruturação da ordem estabelecida.

E isso digo eu, que não estou particularmente interessado em defender "subrotinas". Como é seria a experiência de aprendizado de uma pessoa pessoalmente determinada a gostar de "subrotinas"? Será que esse aprendizado é possível?

Creio que há aqui um defeito no texto, se estiver nele codificada uma intenção de educar; o texto me rejeita, tanto quanto eu o rejeito.

Sábado, Agosto 08, 2009

Novo projeto: monografia sobre o ambiente de programação C++

Como algumas pessoas sabem, um dos meus objetivos de mais longo prazo é projetar um sistema operacional em C++ e aplicar nesse projeto os métodos da orientação ao objetos, da programação genérica e da programação funcional.

Seria, é claro, ingênuo simplesmente acreditar que essa atividade é como programar uma variante do Unix convencional, meramente usando o compilador C++. A linguagem impõe requisitos muito mais interessantes sobre a ABI do sistema que C, uma linguagem simplória nesse sentido.

Assim sendo, comecei a estudar a questão da ABI do sistema e como C++ lida com ela, bem como o suporte ao mecanismo central da programação genérica em C++, os templates.

Tudo isso me sugeriu este novo projeto: escrever uma monografia sobre o ambiente completo de programação em C++, evidenciando detalhes ocultos ao programador, como a atuação do linker, do assembler, do compilador, do loader e mesmo de drivers de compilação como o make.

Quem sabe esse não será meu primeiro livro publicado?

Quinta-feira, Agosto 06, 2009

Comentário do dia

-- Você não sabe como é bom. É bom demais. Nego me paga pra ler um livro. Sabe como é?

Sábado, Agosto 01, 2009

Sigyn: componentes genéricos para networking

Gostaria de anunciar o lançamento da biblioteca Sigyn.

A Sigyn contém diversos componentes genéricos para o desenvolvimento
de servidores de rede concorrentes. Ela também contém exemplos para
protocolos básicos como chargen e discard.

O código-fonte da biblioreca está disponível neste repositório
Subversion:

http://ccppbrasil.googlecode.com/svn/users/pedro.lamarao/sigyn

Documentação sobre a biblioteca está disponível aqui:

http://code.google.com/p/ccppbrasil/wiki/Sigyn