Arquivos | computação RSS for this section

Suggestion: 8+1 languages worth learning

Introduction

Every now and then I have contact with a few programming languages and this is the subset that I believe it would give me a very close insight to the sum of the all languages that I’ve had contact with. Also, this subset is not only based on the choice of ideas that each language aggregate, but also on their usefulness and importance for the general programmer’s toolbox.

Regex

Just about the most awesome way to describe and manipulate words from regular languages. No matter if it’s used as communication purposes within some specification or if it’s used to crawl certain patterns within a large collection of texts. It’s useful even within the programming environment itself. And to contribute to its awesomeness, it’s one of the easiest and fastest things to learn. It’s useful even for non-programmers (think about that time when you want to rename all files from a folder to have better consistency).

You can visualize regex patterns using Regexper or any of its competitors.

MarkDown/CommonMark

Started as a simple tool to pretify common syntax used in text-based email. But now just about almost every major site visited by programmers (e.g. StackOverflow, Reddit, GitHub, Doxygen-generated ones) has some support for MarkDown. Or its recent attempt for a smart standardization to spread good common practices and inspire better interoperability among supporting tools.

You can think of MarkDown as a simple way to describe which parts of the text will be bold or will be the tittle for a subsection and so on. MarkDown is simple! MarkDown is simple enough to be accepted in non-programmer targeted products like blogging platforms (even WordPress) or discussion platforms.

C

A language that appeared in 1972 that is still interesting and it’s still important. Being the “portable Assembly”, operating system’s kernels are still written in C. Pieces of software dealing with low-level are still written in C. Embedded projects are still written in C.

C is not a popular language out of merits. C is just the right abstraction to forget about Assembly, but still have no overhead between your software and the machine. Compilers will do a fantastic job in no time for you.

C is an easy language to learn, adding just a few handful abstractions like subroutines and structures to learn. Of course, C is very low-level and you’re expected to face manual memory management (and memory leaks), bit by bit serialization, pointer to functions (no closures here), architecture and operating system differences and maybe things like varargs, setjmp and mmap. You should be able to understand the implications on performance some decision has. This insight is something C has been a great language at and will hardly be acquired learning another language.

Haskell

Haskell is one of the languages I learnt this year. It’s a typed purely functional language. It’s a great language. It has great concepts to decrease the total number of lines of code you should write (like list comprehensions and pattern matching), a clever syntax and some great concepts you could learn (higher-order functions, currying, lazy evaluation…).

Not all about Haskell was new to me, as I had already learn functional programming through Scheme some years ago, but Haskell does a much better job. I hate Lisp naming conventions (car for the head of the list, seriously) and excessive number of parentheses. You shouldn’t have to follow my path. You should be introduced to functional programming with Haskell.

Also, look at how elegant this QuickSort is:

Ruby

Ruby is another of these languages I learnt this year. It’s a purely object-oriented language. Some cleverness was invested around its syntax and I very much appreciate this. It’s a very dynamic language where every class is open and even things like attr_reader are methods.

Object-oriented programming is one of these must-have skills for a programmer and I think Ruby, being purely object-oriented, is a great language to learn this paradigm. Hide and encapsulate!

I choose to learn Ruby looking for a scripting language to empower a possible game engine that I might code. Ruby really impressed me. Ruby is so dynamic that even if I design a wrong class hierarchy or something, Ruby probably has a way to fix it. I don’t intend to design bad hierarchies, but I don’t know who will use my possible future game engine and this concern then becomes undeniably important.

JavaScript

One of the worst languages I’ve ever seen. But also one of the best languages I’ve ever seen (yep, out there you can find programming languages that would surprise you in the bad way). This language is not what I’d like to be the most popular, but it’s just enough to not be hated. Also, it runs on about every web browser, which is like… everywhere. Importance and interoperability. It’s like you really need to know JavaScript.

JavaScript is like the assembly for the web. You’ll find many tools that translate source code from some language into JavaScript just to enable execution within the browser. Once developed to the browser, JavaScript has grow since and now it’s popular even on the server-side. JavaScript also conquered the smart-side.

Not knowing anything about JavaScript is almost like not knowing how to read in the programming industry. It’s a terrible language full of bad decisions, but it’s the common denominator of the web development.

Learning JavaScript also may help to solidify concepts you should know like asynchronous APIs, JSON and some others.

XML/HTML

Responsible for most of the web traffic, this is a pretty important and simple language to understand how web documents are structured. If you think I’m overestimating web, it’s because it’s one of the greatest things we have. But XML is not only about web, it’s about interoperable documents and protocols and it is used as such. You can find XML in use within vector formats, formats for office applications and even chat protocols. I think learning the basics of XML is a big deal.

LaTeX

I personally think that the LaTeX tools aren’t among the most polished tools. Just look at the Makefile generated by Doxygen to see the run-until-no-more-differences-found loop to work around inconveniences in the LaTeX tools. Or just look at the terrible error messages. Also, the syntax isn’t surprisingly pleasant.

But when you want to focus on the content, forget about the accumulated little formatting details and produce beautiful scientific papers, a book with consistently in-between linked references or even just a few math formulas, LaTeX is probably what you should, at least, consider.

Bonus: bash

Capable to automate the most surprising tasks in a computer, if you are using an Unix variant system. You could automate builds, customize software startup sequences and manage your system. But if you’re using an Unix variant system, you already may be aware of that.

Notes

No Java, C++ or Python in this list. Maybe I’ll do a part 2 of this article containing languages with a lesser chance to be used like SQL, MongoDB, OpenGL, R, GStreamer or some Assembly. Actually, I think Java, C++ and Python have a better chance to be used than Haskell, but if you learn every language in this list, C++, Java and Python will be easy to catch up and the lines of code you write will be more elegant.

Análise de TAD e POO em Ruby

Recentemente acabei investindo um pouco de esforço para aprender e analisar o suporte que Ruby dá para tipos abstratos de dados e programação orientada a objetos devido a um trabalho que meu professor de paradigmas de linguagens de programação cobrou. Apesar de eu ser iniciante na linguagem e o documento resultante poder conter erros, acho que pode ser interessante para outras pessoas, então estou deixando tal documento sobre a linguagem Ruby aqui.

PHP case: one vision, two approaches

Till today, I didn’t read a post defending PHP. There are all these texts attacking the language. And I dislike most of these texts I’ve read. I don’t like the attacked PHP language either. But what I dislike above all is the excessive use of fallacies. How could we have a logical discussion if we keep using them?

I don’t mind if you share a personal experience that cannot be used to prove a statement. If we’re lucky, your experience might be fun to read or will teach us to avoid specific behaviour in specific circumstances that may apply in specific ages.

I don’t mind if you carefully expose facts that the creators want to hide from us to affect our level of trust to such creators, as long as you use evidences to sustain such facts. You aren’t trying to logically prove something, but you text is also useful.

I don’t even mind if you create a text completely relying on fallacies, but I mind a lot if someone use such text to justify a decision. These texts, to my experience, tend to be fun anyway.

So, there are the two following linked texts about PHP, and in one of two, the author demonstrate more PHP knowledge than the other. Which one deserves more of your trust/attention?

Espaço: o separador natural de palavras

Antes do lançamento da Allegro 5, tinha uma página na wiki deles com uma discussão interessante sobre estilo de API. Tinha uma frase de lá que eu não conseguia esquecer, mas por várias vezes já quis referenciar durante discussões e eu não conseguia reencontrar. Recentemente, acabei reencontrando tal frase e resolvi fazer um post dedicado aos momentos em que eu não usei essa referência. Isso mesmo, eu me rebaixei a fazer um post de dois parágrafos, onde um deles nem é de minha autoria.

Graças ao sublinhado, tudo fica mais legível. “Por que?”, você pergunta. Bem, quando somos crianças e aprendemos a ler, nossos padrões cerebrais se adaptam para reconhecer o espaço (ou o sublinhado, que é graficamente similar) mais facilmente como um separador de palavras.

– tradução livre de trecho encontrando na wiki do Allegro

Palestra: Acessibilidade na web – principais problemas e soluções

Eu sempre me preocupei com acessibilidade, até um certo ponto, mas nessa situação costumei ser o tipo de “preocupado passivo”, não agindo muito mais que o básico. Entretanto, tem uma palestra que ocorreu no FISL14 sobre acessibilidade que me mostrou que um pouco de conhecimento é tudo que você precisa para melhorar muito a acessibilidade do conteúdo que você cria, pois o esforço necessário costuma ser negligenciável (tarefas que você precisa fazer de qualquer forma, mas fazer pensando na acessibilidade).

Enfim, assistir essa palestra é algo que eu recomendo a todos que criam conteúdo na web:

Vídeo: http://hemingway.softwarelivre.org/fisl14/high/40a/sala40a-high-201307051155.ogg

Slides: http://pt.slideshare.net/julianafrost/acessibilidade-na-web-principais-problemas-e-solues

Essa palestra foi o que me ensinou a evitar links do tipo “clique aqui para saber mais” aqui nesse blog. Minha preocupação só diminui quando o conteúdo é inacessível de qualquer forma (filtros de imagens, por exemplo).

Performance em C++

Há várias linguagens que possuem várias abstrações e expõem diferenças de comportamento entre programadores. Se a linguagem for simples o suficiente e não permitir muitas formas de expressar algum algoritmo, então um programador qualquer pode usá-la para se comparar a um programador melhor do que ele, seja em questão de produtividade, segurança, performance, generalismo ou outros. C++ é uma das linguagens que ajuda a destacar os programadores que realmente estão preocupados com performance e essa postagem é sobre conhecimento que eu ganhei no meu aprendizado.

Performance na linguagem C++

Discutir performance em linguagens de programação é uma tarefa fácil de desvirtuar devido a grande quantidade de variáveis que pode interferir na análise. Rodar benchmarks por si só é uma tarefa bem complicada de fazer corretamente, e mesmo assim, os compiladores podem evoluir e invalidar o resultado do benchmark. Analisar o código Assembly gerado pode parecer interessante, pois é “livre de influências externas” (no sentido que outros processos não irão “roubar” tempo de CPU do programa sendo observado), mas as arquiteturas podem evoluir e invalidar o resultado da análise, assim como não existe uma arquitetura definitiva. E caso você ainda não esteja convencido, C++ é só uma especificação e dois compiladores diferentes podem implementar a mesma especificação, mas apresentar resultados diferentes (onde um deles vence uma implementação Java e outro perde). Assim sendo, resta a nós analisar as facilidades que a linguagem fornece para ajudar as implementações a fazer análise do código (onde um código de alto nível que descreve intenção pode resolver o problema de forma mais performática) e facilidades que a linguagem fornece para ajudar o programador a acessar recursos da máquina (onde um código de baixo nível que descreve, por exemplo, padrões de acesso a memória, pode ser mais performático).

Mas antes de continuar, é interessante ressaltar que C++ não é a única linguagem que permite performance. Mesmo linguagens que não são focadas em performance podem incentivar modificações no código-fonte pelo bem da performance. Um exemplo é a diferença entre range e xrange no Python 2. Entretanto, C++ possui um foco bem grande em performance, tendo, por exemplo, o princípio de que “você só paga pelo que você usa”, influenciando em várias decisões da linguagem (métodos não são virtuais/polimórficos por padrão, por exemplo).

Uma característica que foi introduzida para ajudar as implementações a produzir código mais performático é a propriedade Undefined Behaviour (comumente citada somente como UB). A especificação da linguagem C++ define que as implementações são livres para se comportar de qualquer forma em alguns erros de programação específicos (que em alguns casos só podem ser detectados em tempo de execução), como acontece, por exemplo, no caso do índice em vector. Tal liberdade permite que a implementação não gere qualquer código para detecção de erro e gere código ainda mais performático. Isso torna C++ uma das linguagens em que não é encorajado que você aprenda orientado a compilar-e-observar, pois caso esbarre em UB, não há garantias de que o mesmo ocorre em diferentes compiladores (ou mesmo diferentes versões do mesmo compilador!). Torna-se até interessante que você execute os testes unitários sob a análise de “address & memory sanitizers” e talvez usando bibliotecas STL alternativas que foram projetadas para detectar UB. Recomendo ler essa questão no StackOverflow e esse post para saber mais. Esse é um exemplo onde a linguagem ajuda a implementação.

Outro exemplo onde a linguagem ajuda a implementação é a funcionalidade restrict, que é um especificador para ponteiros que informa a implementação que não ocorrerá aliasing para aquele caso. “Banir” aliasing informa que um ponteiro aponta para uma região de memória que não é apontada por nenhum outro ponteiro, então se você modifica uma porção de memória através de um outro ponteiro, não precisa se preocupar com a possibilidade dos dados apontados pelo primeiro ponteiro terem sido alterados. Veja o exemplo de código abaixo, onde, quando é sabido que aliasing não pode ocorrer, o retorno da função pode ser inferido em tempo de compilação e menos instruções Assembly são geradas. Um caso legal é a linguagem Rust, ainda em desenvolvimento, que detecta race conditions em tempo de compilação usando um modelo de estados que captura padrões de acesso/modificação interessantes e “proibe” aliasing.

Confesso que, para mim, a necessidade da palavra-chave restrict demonstra um possível mal planejamento nesse caso extremo, mas C++ é sobre performance, inclusive nos casos extremos. Felizmente linguagens como Rust estão dando um passo à frente. Um outro, dentre esses casos que vejo como “extremos”, mas imensamente mais popular, é a técnica “Empty Base Optimization”. Como essa é uma técnica que merece seu próprio post, me limito a deixar duas referências e informar que essa técnica é um grande justificador para o padrão permitir herança privada.

Há os casos onde a informação necessária para otimização não está disponível no código-fonte. Em tais casos pode-se aplicar JIT ou PGO, mas a informação definitiva ainda pode estar “nas mãos” do programador. Em tais casos onde o programador detém a informação definitiva, há o exemplo do std::vector::reserve, especificado pelo padrão do C++, e o exemplo do likely, extensão extra-oficial. Esse é um exemplo onde a linguagem ajuda o programador, especificando vocabulário para guiar as abstrações a fazerem um trabalho melhor.

“Você só paga pelo que você usa”. Não só decisões são pensadas para levar em conta o caso com menos custo, como novas abstrações são criadas para adicionar pontos de variação que permitem especializar um comportamento de acordo com o seu uso comum. Um exemplo disso é o conceito de allocators, que permite customizar algoritmos de alocação dos containers fornecidos.

E como se tudo isso não fosse o bastante, há ainda a possibilidade de mover uma pequena parte da computação para tempo de compilação através de templates (que são Turing-complete), com o uso de técnicas como “expression template“, por exemplo. Eu já usei templates até para explorar o fato do compilador eliminar código-morto em tempo de compilação (em vez de fornecer duas implementações diferentes, capturava o comportamento comum e passava o argumento booleano como argumento de template, em vez de argumento de função), mas esse é um uso desencorajado.

Você pode fazer várias modificações em código-fonte escrito em outras linguagens pelo bem da performance, mas dificilmente você encontrará um suporte a performance, nas outras linguagens, tão abrangente quanto o disponível em C++. Uma ideia que eu tenho é de usar os critérios que eu coloquei aqui para fazer a análise a fundo (biblioteca padrão, expressões, garantias da especificação que previnem código mais performático) de C++ e umas 3 outras linguagens (provavelmente Lisp e Java estariam nessa lista) para publicar algum paper.

Profiling

As otimizações mais importantes são otimizações no algoritmo, pois elas são universais. Otimizações no próprio código podem ficar “obsoletas”, no sentido que os compiladores e arquiteturas de computadores podem evoluir e deixar a sua “otimização” inútil (ou até pior!). Para acompanhar a evolução, é interessante implementar a versão correta do algoritmo primeiro e compará-la contra suas otimizações. Esse é o papel do profiler, desmistificar “preconceitos” e “superstições” que o programador tenha.

Uma outra utilidade para o profiler é aumentar a eficiência do programador que está interessado em otimizar o código, pois o profiler não só dará informações úteis para realizar as otimizações, como também dará, por exemplo, a informação de que pedaço do código é usado 90% do tempo, ajudando o programador a focar na parte que importa.

Há a palestra Three Optimization Tips for C++, do Andrei Alexandrescu, onde ele usa frases como “measuring gives you a leg up on experts who don’t need to measure” e “you can’t improve what you can’t measure”, além de discutir “dicas/exemplos incomuns”.

Em questão de ferramentas para fazer profiling, há o sempre citado conjunto de ferramentas do projeto Valgrind, a ferramenta pahole e muitas outras. Para tarefas muito simples, pode ser que uma solução feita por você possa funcionar bem, enquanto para algumas outras tarefas, você vai ter que pesquisar até encontrar a esotérica ferramenta capaz de lhe ajudar.

A arte de realizar benchmarks pode não ser óbvia, muitas vezes.

Assembly

Não, você não vai programar em Assembly, mas benchmarks podem ser influenciados por diversos fatores e uma informação que não mente é o código-fonte em Assembly. Você vai querer usar Assembly para saber se há alguma abstração que seu compilador não está otimizando bem, alguma otimização sua que é contraprodutiva ou alguma funcionalidade da arquitetura que está deixando de ser utilizada (como vetorização, por exemplo). Há até os momentos onde o compilador pode ter um bug e a última peça para confirmar a informação está no código-fonte Assembly.

Há a palestra Preparing the GNU/Linux for 64-Bit ARM Processors, onde o Jon “Maddog” Hall comenta o caso dos programadores do X, que conheciam tão bem o compilador GCC, que quando um grupo tentou usar um compilador alternativo, que normalmente gerava código 30% mais eficiente, o ganho foi zero (termo que o Maddog usou).

Eu demorei um tempo para entender Assembly e só acabei aprendendo depois que fiz um curso de compiladores no Coursera. Eu não sou um bom programador de Assembly, mas minha recomendação para quem quiser aprender e ainda não sabe de nada é fazer um curso de compiladores e outro de sistemas operacionais.

Eu deveria ter preparado mais referências para essa seção, mas quando elas primeiro me cruzaram, eu não estava planejando escrever esse texto, então eu as perdi. Caso você tenha algum relato interessante, contribua na seção de comentários.

Branch misprediction

Com a evolução dos computadores, a performance da CPU foi uma das que mais evoluiu, e evoluiu muito mais do que a performance de acesso a memória. Para lidar com o problema, arquiteturas modernas fazem o uso de várias técnicas para continuar alimentando o monstro que é a CPU a todo o momento.

Uma das técnicas utilizadas é o uso de cache (e write buffers) para evitar acesso a memória quando o valor tiver sido recentemente utilizado. Os caches costumam estar próximos a CPU, terem baixa latência para serem acessados e pouco espaço para armazenamento. O termo cache locality é normalmente usado para se mencionar um algoritmo que faça todas as computações com o dado antes de prosseguir para o próximo.

A performance da memória foi evoluindo mais com o aumento da largura de dados do que com a diminuição da latência. Capturar um dado continua sendo caro, mas através de outra técnica, prefetch, que se resume ao sistema tentar prever qual o próximo de bloco de memória que tornar-se-á necessário e requisitá-lo antes do tempo (à lá AOT), esse custo aparece com menos frequência. Essa otimização em arquiteturas de computadores modernas beneficia algoritmos que possuem padrões de acesso a memória mais fáceis de prever.

Esses comportamentos de arquiteturas modernas de computadores são úteis para realizar várias otimizações, mas para essa seção, o efeito que quero comentar é o efeito de branch misprediction.

Não só os próximos blocos de memória para dados são requisitados antes do tempo, como também os próximos blocos de memória contendo instruções também o são. Normalmente as instruções são executadas na ordem em que estão armazenadas, tornando fácil de fazer a previsão. Uma das instruções que quebra essa ordem é o pulo incondicional (para chamada de subrotinas, por exemplo), mas esse também é fácil de prever.

O efeito de branch misprediction torna-se possível quando há um pulo condicional (um loop, um if…). Se das últimas vezes a instrução sempre entrou no “bloco true”, o prefetcher pode escolher pré-carregar o bloco true na próxima oportunidade. Se a predição estiver correta, ótimo. Se o prefetcher prever errado, a CPU vai ter que parar para esperar uma das instruções mais caras terminar de executar: acesso a memória.

Eu já cometi o erro de otimizações contraprodutivas explorando multiplicação por um booleano para evitar branch misprediction. Acontece que existe, para algumas arquiteturas, a instrução CMOV, que condicionalmente move um valor. Não existe pulo de instrução nenhum e o código é mais fácil de ser analisado pelo prefetcher, evitando uma ocorrência de branch misprediction. Então nada de ficar reimplementando as funções max, min e outras em um namespace branchless. Você tem que ficar de olho é em código-fonte cujas ramificações tenham código complexo. Em caso de dúvida sobre o comportamento, verifique o código Assembly gerado.

Há essa grande resposta do StackOverflow sobre branch misprediction para quem ainda quiser saber mais.

Padrão de acesso a memória

Graças a forma como arquiteturas de computadores modernas se comportam, você pode estar interessado em mudar o algoritmo para que ele itere sobre os dados em uma ordem diferente. As ideias por trás das otimizações das arquiteturas modernas são simples de entender, mas o custo de ignorá-las pode parecer muito abstrato e as formas de explorá-las podem acabar sendo bem criativas.

O assunto me lembra padrões de projetos orientado a objetos, onde você pode explicar o básico, mas as pessoas podem continuar se surpreendendo com as formas criativas que são usadas para explorar esses conceitos básicos. Há duas palestras muito boas sobre o tópico para as quais eu não acho que eu seria tão bom quanto tentando expor o assunto. Essas excelentes palestras são:

Assuntos polêmicos

Garbage collection tem desempenho melhor, pois aloca memória em grandes blocos

Costumo encontrar o argumento de que Garbage Collection pode melhorar a performance, evitando alocação e desalocação de vários objetos pequenos com frequência. Bom, se esse argumento for verdade, desde o C++11, é liberado o uso de implementações que façam uso de Garbage Collection, mas vou discutir isso depois, pois a crítica não é a ausência de Garbage Collection, mas sim o algoritmo de alocação.

Normalmente você evita alocar na heap e costuma usar a stack, pois trabalhar na stack se resume, em níveis de implementação, a manipular o ponteiro da stack, que é uma operação muito barata. Normalmente você só usa a heap para (1) objetos cujo tamanho não são conhecidos até que recebam a entrada do mundo externo e (2) para objetos cujo tempo de vida excedem o escopo da função. É comum usar a heap, mas não se costuma usar a heap para objetos pequenos e que têm o tempo de vida limitado ao escopo da função.

Em resposta a quando o problema de desempenho é o algoritmo de alocação, C++ permite, sem recorrer a GC, customizar esse algoritmo usando allocators e até sobrecarregar os operadores new e delete.

Em resposta ao argumento de Garbage Collection (e não algoritmo de alocação) ser mais rápido, no modelo RAII, a implementação já possui a informação de quando um recurso torna-se inválido e pode ser liberado, então como pode ser mais eficiente jogar essa informação fora e, forçadamente em tempo de execução, executar um algoritmo para recuperar a informação que já existia antes? E para aumentar a crítica, os algoritmos de GC costumam não possuir tanta informação sobre o ambiente (daí que vêm os algoritmos conservadores de GC), o que impede que eles possam dar garantia de que sequer os recursos serão liberados (a principal diferença semântica entre destrutores e finalizadores) e pode até dar uma base de dados para computar maior, o que torna a tarefa ainda mais custosa. Parece haver um problema de lógica no argumento, mas para aumentar ainda mais a crítica, o modelo RAII é mais amigável ao cache, pois expõe um modelo que faz objetos que são usados em tempos de uso similares serem alocados próximos uns dos outros.

Um outro argumento a favor de GC é que há o custo de chamada de função. Num mundo GC, vários objetos são coletados com uma chamada de função, mas no mundo RAII, o custo de chamada de função se acumula. Então, seguindo essa lógica, o tempo total (amortizado) para GC seria menor, enquanto que seu tempo de resposta seria pior, pois quando o GC entrasse em ação, muito tempo seria gasto na liberação de recursos. Para melhor discutir esse argumento, precisamos separar os conceitos de alocação e construção de recursos, que se referem a obter uma região de memória para o objeto e colocar o objeto em um estado válido, respectivamente. C++ permite construção (e destrução) barata em vários casos (leia sobre inline e POD), mas no que se refere a alocação, esse argumento está correto. Chamadas de objetos individuais são feitas para a função free. Funções alternativas a malloc/free que aloquem grandes blocos de memória não podem escapar desse comportamento na hora de desalocar.

Daí a questão se torna, “será que o gargalo do algoritmo de GC é mais barato que a soma das várias chamadas extras de funções para desalocação”. Para responder essa questão de forma genérica, muita abstração é necessária, dificultando o trabalho para respondê-la. Mesmo na versão fácil, onde só considerarmos uma comparação direta entre duas implementações específicas, há trabalho que eu não quero fazer para responder uma única pergunta, que nem é objetivo central desse texto. O que eu posso comentar para não fazer esse parágrafo lhe desapontar tanto, é que se você tem um std::vector com 100 elementos, quando ele for destruído, apenas uma chamada a função de desalocação será feita para os 100 elementos. C++ lhe dá o controle, e com ele você pode otimizar. E para você não ficar com a impressão de que eu sou um programador desinformado, quero adicionar que sim, eu sei que o custo de chamada de funções é barato, normalmente negligenciável.

Há também os projetos de embarcados usando microcontroladores, nos quais você não verá o uso de linguagens Garbage Collected com frequência. Usar uma implementação que se apoie unicamente em GC para, por exemplo, reproduzir áudio, pode ser uma ideia colossalmente estúpida.

Exceções são lentas

Os velhos comentam que as primeiras implementações de tratamento de exceções em C++ eram lentas e esse fato contribuiu para a aversão que alguns programadores possam ter em tratar erros usando exceções. O documento TR18015 detalha duas abordagens principais para a implementação de suporte a exceções e o que o documento menciona como “abordagem tabela” é muito barata, realizando boa parte do que precisa ser computado em tempo de compilação e estruturando o código de forma que não há penalidade em tempo de execução para o fluxo de execução normal do programa. O custo, nessa abordagem, é que o programa fica maior, mas a parte extra pode ser movida para não interferir negativamente no uso de cache e, em sistemas com suporte a memória virtual, essa parte pode até ficar na swap até que torne-se necessária.

uma resposta no StackOverflow, caso você não queira se dar ao trabalho de passar pelo TR18015. A implementação do suporte a exceções é bem elegante e a discussão deveria deixar de ser se exceções são lentas e passar a ser quando usar exceções.

E cuidado com os benchmarks injustos que alimentem esse mito, assim como o P. alerta.

Algoritmos

E finalmente, mesmo explorando várias possibilidades disponíveis em C++, que dificilmente você encontrará em outras linguagens, ainda resta a opção de melhorar o próprio algoritmo para fazer seu código-fonte executar mais rápido. Estudando o algoritmo a fundo talvez você encontre meios de eliminar operações devido a características especiais de como as diferentes partes são combinadas, a entrada do programa ou outros. Talvez você encontre um algoritmo que se ajuste melhor ao caso comum da sua entrada.

Em códigos-fonte que escrevi, eu já cheguei a mesclar duas etapas em uma para evitar uma segunda passagem sobre os dados, eliminando o custo de incrementar o iterador uma segunda vez para cada elemento do container (e ao mesmo tempo fazendo melhor uso do cache, ao acessar elementos enquanto ainda estão “quentes”). Um tapa na cara da filosofia UNIX.

Hardwares especiais

C++ é certamente uma ótima linguagem para alcançar uma implementação eficiente de qualquer algoritmo para rodar em sua CPU, mas há hardwares dedicados e de propósito específico para os quais não usamos C++. Para explorarmos um sistema gráfico 3D, possivelmente otimizado por uma GPU, usaremos OpenGL/GLSL. Para explorarmos GPGPUs, usaremos OpenCL (padrão aberto dominante) ou CUDA (padrão proprietário dominante). E há ainda outras tecnologias que você talvez vá precisar aprender tais como FPGA, Verilog e VHDL. Eu não posso estender muito o tópico, pois ainda estou bastante preso na terra do C++, mas talvez em um futuro post eu o faça. Mas ao menos eu já mexi com Arduino, e no Arduino, você ainda vai usar C++.

Eu li em algum lugar que a galera que não pode dispor de um ambiente de computação real time (que inclui o kernel) costuma comprar hardware que implemente a interface MIDI e controlá-lo via software, possivelmente escrito em C++, para criar músicas ao vivo.

Para quem quiser seguir essa estrada, pode ser interessante verificar o laptop Novena.

Comparação com outras linguagens

Se você mostrar o código Assembly para uma função de soma feito na linguagem X, que é tão bom quanto o código Assembly equivalente para C++ e tentar argumentar que isso torna sua linguagem tão performática quanto C++… bom, se nem para esse exemplo, que estressa tão pouco técnicas de performance, a sua linguagem se comparasse a C++, ela seria humilhada em situações mais complexas.

E sua linguagem não ter foco em performance NÃO é ruim. É legal que a implementação da sua linguagem seja rápida ou tão rápida quanto possível dentro de sua proposta, mas se o foco da sua linguagem é segurança, então é sobre a segurança de sua linguagem que você deveria estar fazendo propaganda… não sobre performance. Eu fui teimoso por bastante tempo e demorei a ser mais pragmático.

Conclusão

Se você está seriamente preocupado com performance, você vai precisar aperfeiçoar suas habilidades em múltiplas áreas em vez de ter um falso senso de trabalho bem feito ao aplicar uma única dica que você aprendeu no único texto sobre otimização que leu na vida, otimizando a função que só é chamada uma vez durante toda a vida útil do processo ou aproveitando seus conhecimentos sobre o domínio da aplicação. A chave para o sucesso é a combinação de todas as relevantes áreas.

Esse texto ficou tão grande que eu fico imaginando se há alguém paciente para lê-lo por completo.

Opinião: No curso de computação eu uso cheat, no curso de computação eu uso Linux!

Há esses pequenos detalhes que eu percebo na parte da realidade a qual eu tenho acesso e talvez se repita em outros lugares. Após um tempo “martelando a ideia”, resolvi sentar para escrever o relato acompanhado de minha opinião. Nesse texto vou comentar sobre o porquê de eu achar que ter optado pelo Linux me deu vantagem sobre outros programadores do curso que faço.

A linguagem C

A primeira coisa a entender na história é a importância da linguagem C. A linguagem C é uma espécie de “Assembly portável”. Entre o código escrito pelo programador e o código gerado pelo compilador não deveria haver nenhum gargalo desnecessário. O código gerado deveria ser tão ou mais rápido que o código que o programador escreveria manualmente. Uma das formas de alcançar essa eficiência é através de Undefined Behaviour, pois assim, entre outras coisas, o código se adapta ao comportamento do hardware em muitas situações. Em C, até a ordem de avaliação dos operandos não é especificada.

Com sua proposta, a linguagem C tornou-se uma das principais linguagens no mundo da computação. Sistemas operacionais são construídos na linguagem C, sistemas para dispositivos embarcados são construídos na linguagem C, sistemas com restrições de tempo real são construídos na linguagem C, para citar alguns. Devido aos sistemas operacionais serem, principalmente, escritos em C, normalmente a linguagem C possui tratamento privilegiado em relação a comunicação com o sistema operacional e normalmente até os interpretadores de outras linguagens são escritos em C. Devido a sua importância, aprender C ajuda a ter uma visão geral dos diferentes componentes que compõem o sistema. Não só é interessante aprender C por sua importância, mas também porque a linguagem expõe conceitos interessantes que não estão presentes em linguagens que fogem do hardware e se aproximam demais com definições algorítmicas elegantes, como os conceitos de escovar bits, alocação de memória dinâmica, o custo de uma chamada recursiva, o uso de aritmética de ponteiros em estrutura de dados, entre outros.

Um fato bem importante para a história que estou tentando contar é que o processo de compilação de um programa escrito em C é complexo. Não sendo suficiente tal processo ser complexo, há muitos comportamentos que não são padronizados e podem tornar incompatíveis códigos-objeto gerados em compiladores diferentes. Ilustrando o caso, considere a opção -fpack-struct, que melhora o uso de memória, mas quebra a compatibilidade com códigos-objeto compilados sem essa opção (o que pode incluir bibliotecas do sistema). A Boost, por exemplo, lista uma página contendo uma conta simples para estimar que há 3200 variações de ABI (claro que Boost é escrita em C++, onde o problema é maior). Isso é só para exemplificar que variações de ABI existem, mas compiladores diferentes normalmente usam ABIs diferentes.

A falta de uma ABI “para a todos trazer e na escuridão aprisionar” é um problema quando você não controla as APIs que deseja utilizar e suas respectivas disponibilidades são limitadas. Citando APIs open source, tem a Qt, que no momento em que escrevo disponibiliza pacotes compilados com MingW, VS 2010, VS 2012 e VS 2013, enquanto a SDL disponibiliza somente para uma versão não claramente especificada do Visual C++ e do MingW. APIs open source são APIs que você controla, pois você sempre tem a opção de compilar sua própria versão.

Apesar de não haver uma ABI estável, no Linux, grande parte das distribuições adota a ABI padrão do GCC, que é bem estável, mesmo dentre diferentes versões do GCC. Ainda no ambiente Linux, compiladores competidores, como o Clang, também tentam aderir a ABI do GCC. Com isso, podemos usar qualquer biblioteca já instalada em nossos sistemas sem problemas causados por diferentes ABIs e também não ficamos dependentes de uma ou outra versão muito específica do Visual Studio.

Outro problema com o complexo processo de compilação de um programa escrito em C é a questão “onde encontro os arquivos de cabeçalho durante a fase de compilação e as bibliotecas na fase de linkedição?”. O Linux é um sistema que pode respeitar o padrão de sistemas de arquivos hierárquicos, que define um padrão sano para encontrar esses (e outros) arquivos-chave do sistema. O Windows próprio não fornece algo próximo ao padrão FHS, então a configuração para compilar algo fica por responsabilidade do compilador/versão-do-compilador/projeto/usuário, sempre tornando o processo mais complexo.

Todo esse complexo processo é bastante facilitado, não somente através do uso de software livre que você pode controlar, mas também, no Linux, através de gerenciadores de pacotes. Os gerenciadores de pacotes não só facilitam o gerenciamento de um sistema, como também automatizam a maior parte da preparação de um ambiente para programar em C (e muitas outras linguagens também). Há até as distribuições que não precisam de pacotes extras de desenvolvimento, tornando o processo ainda mais fácil. Se você tem o Qt ou algum programa que dependa do Qt instalado, significa que você tem o Qt instalado, em toda sua extensão, e já pode começar a programar software que faça uso do Qt. Usuários iniciantes no Linux costumam manter a mesma abordagem de instalação que usavam em seus sistemas anteriores, procurando o site do produto como o primeiro passo em um processo de instalação NNF. Alguém me salvou dessa filosofia com uma simples pergunta há bastante tempo atrás, “qual o sentido em possuir um gerenciador de pacotes se você não usa ele para gerenciar os pacotes?”.

E é isso! O nível de dificuldade imposto pelo ambiente mais usado no meu curso desencoraja o desenvolvimento, aprendizado e qualquer modificação em projetos escritos na linguagem C, apesar de ela ser uma das mais importantes e expor muitos conceitos importantes.

Existem outras tarefas mais dignas para perder tempo do que ficar aprendendo C, pois algoritmos são mais “puros” e aplicáveis a outras linguagens além de C. Otimizações em algoritmos são universais, mas um fato que os advogados dessa filosofia gostam de ignorar é que você não projeta algoritmos para rodar em máquinas abstratas e se não for possível executá-los em máquinas de verdade, eles perdem sua principal utilidade. Conhecimento de arquitetura de computadores é importante e se otimizações a nível de algoritmo fossem a única coisa a importar, talvez não houvessem tantas bibliotecas de álgebra linear tentando ser a mais rápida, assim como não haveria artigos como o “Why Computer Architecture Matters” ou palestras como “Native Code Performance and Memory: The Elephant in the CPU” ou ainda conceitos como “Cache-oblivious algorithm“.

Abertura do sistema

Outra característica que acho que me favoreceu durante meu aprendizado é a disponibilidade fácil de documentação e discussões de desenvolvimento abertas.

Quando há um padrão disponível, ele precisa estar bem documentado e sem nenhuma ambiguidade, mesmo que ao custo de tornar a leitura mais difícil. Pelo bem da interoperabilidade, padrões já existentes são adotados e a criação de novos padrões é evitada. No outro “lado da batalha”, incentivos são feitos para estratégias de lock-in e DRM. Isso é uma discussão bem maior, que não é do interesse desse texto. A característica interessante para os argumentos que estou construindo nesse texto é que você sempre pode descobrir “o que há por trás de alguma tecnologia” e, quando tiver sorte, vai até poder ler um documento bem detalhado, que é a prática encorajada no “nosso lado”.

Considerando os cenários onde uma boa documentação não está disponível, você ainda terá a possibilidade descobrir o que está acontecendo, pois o código-fonte é livre e você não vai acabar esbarrando em um LPVOID lpReserved. Pode ser até que você receba ajuda de pessoas mais experientes.

Um hábito da comunidade que apoio que me ajudou é o hábito de manter as discussões abertas. O ato de declarar derrota quando sua solução não é a melhor. As discussões que ajudam a entender como decisões de projeto afetam um produto, discussões de levantamento e análise de problemas e suas soluções, discussões sobre abordagens alternativas. São essas discussões que ajudaram a moldar a forma como encaro os meus próprios problemas. Alguns exemplos de tecnologias que geram discussões, para efeito de ilustração:

Ainda para efeito de ilustração, um assunto sobre o qual eu estava lendo recentemente é programação assíncrona e, graças ao fato de eu estar participando de várias listas de discussões, acabei encontrando esse texto sobre programação assíncrona e, especialmente, o gargalo que as interfaces podem criar no sistema operacional. Esse nível de detalhamento dos problemas não é algo que eu encontro em sistemas fechados, até porque manter seus detalhes disponíveis publicamente é um acontecimento que eles evitam.

Resultado

O resultado que observo é um monte de programadores incapazes de programar em C, reclamando de projetos que tenham muitas dependências (ou somente considerando bibliotecas header-only), incapazes de compilar outros projetos, optando por linguagens e ambientes inchados para ter certeza que todas suas necessidades são supridas, para citar alguns.

Outro resultado que observo é que você ganha o título de “usuário ninja do Linux” só por ler a documentação e conseguir fazer um programa mal feito funcionar e, para um curso de computação, essa é uma tarefa broxante.

E por essas e outras é que considero que estou “cheatando” por estar usando Linux. Não é que é impossível você conseguir tanto e até mais conhecimento sem fugir da sua zona de conforto e sem usar um sistema operacional alternativo, mas é que eu acho que a jornada é muito facilitada a partir do momento que você adota o Linux.

%d blogueiros gostam disto: