Qual dos dois você prefere?

Aqueles que trabalham com C/C++ costumam, hoje em dia, preferir C++ ao C. Um dos motivos é a facilidade com que podem lidar com strings usando o container basic_string da STL (Standard Template Library). Pretendo mostrar que C++ não somente gera código mais “obeso”, mas também que o código “default” apresenta falhas que não são óbvias para o programador.

Meu exemplo é um simples programa que varre uma string procurando pelo caracter ‘.’ e imprime duas strings separadas por este caracter. Em C seria algo assim:

#include <stdio.h>
#include <string.h>

int main(int argc, char *argv[])
{
  char s[] = "frederico.pissarra";
  char *p;

  p = strchr(s, '.');
  if (p != NULL)
  {
    /* Marca o fim da primeira substring. */
    *p++ = (char)0;

    /* A segunda substring já termina em zero.
       Não precisamos nos preocupar com ela. */
    printf("%s\n%s\n", s, p);
  }
  else
    puts("Cannot find '.'");

  return 0;
}

Ao executar o programa as strings “frederico” e “pissarra” são mostradas em linhas separadas.

A primeira diferença entre C e C++ é que a primeira não possui tratamento de exceções através de palavras-reservadas como try e catch. É preciso realizar testes com valor de retorno de funções para garantir que tudo ocorreu bem. No caso de strchr(), se obtivermos um ponteiro NULL isso quer dizer que a função não achou o token.

O código equivalente em C++, usando o template basic_string ficaria assim:

#include <iostream>
#include <string>

using namespace std;

int main(int argc, char *argv[])
{
  string s = "frederico.pissarra";
  int pos;

  pos = s.find('.');
  if (pos != string::npos)
  {
    string s1, s2;

    s1 = s.substr(0, pos);
    s2 = s.substr(pos+1, string::npos);

    cout << s1 << '\n' << s2 << '\n';
  }
  else
    cout << "Cannot find '.'\n";

  return 0;
}

Parece ser um código mais simples, né? Apenas uma verificação precisou ser feita (se encontramos ou não o ‘.’). Só que C++ esconde um monte de coisas de você…

Primeiro: Todo tipo string é um objeto que contém um ponteiro para char alocado dinamicamente. Ainda, todo objeto precisa ser construído em runtime. Difernte de C++, tudo o que o código em C faz, quando atribuímos a constante “frederico.pissarra” ao array ‘s’ é copiar um conjunto de chars para dentro do buffer e colocar um byte zero no final. Isso é determinado em tempo de compilação. Eis o início do código, em assembly:

...
mov rdx,0x7261737369702E6F
mov rax,0x6369722656465766

/* Copia a constante para a pilha */
sub rsp,32
mov [rsp],rax                /* "frederic" */
mov [rsp+8],rdx              /* "o.pissar" */
mov word ptr [rsp+16],0x6172 /* "ra" */
mov byte ptr [rsp+18],0      /* '' */
...

Neste caso a string “frederico.pissarra” sequer existe como string de fato! É só uma sequência de bytes colocados diretamente na pilha (por que o nosso array é, vejam só, uma variável local!).

Isso ai leva uns 5 ou 6 ciclos de máquina para ser executado.

O código equivalente em C++ não é tão simples e dá algum trabalho para analisá-lo. Basicamente o que o compilador faz é criar a string “frederico.pissara” no segmento de dados read-only, criar o objeto ‘s’ do tipo basic_string e copiar a sequência de bytes para dentro do objeto criado. É algo mais ou menos assim:

  .section .rodata,
.LC0:
  .string "frederico.pissarra"

  .section .text

  ...
  sub rsp,48
  mov rsi,LC0
  mov rdx,[rsp+47]
  mov rdi,rsp
  call _ZNSsC1EPKcRKSaIcE
  ,,,

Eis uns 3 ciclos de máquina só para ajustar os parâmetros para a chamada ao construtor _ZNSsC1EPKcRKSaIcE (não foi um gato que passou pelo meu teclado… o nome da função é esse mesmo!). Esse contrutor toma, pelo menos, uns 10 ciclos só para fazer o CALL e o RET, sem contar com a penalidade relativa à limpeza do cache L1 e os muitos ciclos de máquina que o próprio construtor consome. Só para ter uma idéia, o código em C++ possui diversas referências à pthreads (mesmo que não estejamos usando NADA relativo a threads aqui!):

  .section  .ctors,"aw",@progbits
  .align 8
  .quad _GLOBAL__sub_I_main
  .local  _ZStL8__ioinit
  .comm _ZStL8__ioinit,1,1
  .weakref  _ZL20__gthrw_pthread_oncePiPFvvE,pthread_once
  .weakref  _ZL27__gthrw_pthread_getspecificj,pthread_getspecific
  .weakref  _ZL27__gthrw_pthread_setspecificjPKv,pthread_setspecific
  .weakref  _ZL22__gthrw_pthread_createPmPK14pthread_attr_tPFPvS3_ES3_,pthread_create
  .weakref  _ZL20__gthrw_pthread_joinmPPv,pthread_join
  .weakref  _ZL21__gthrw_pthread_equalmm,pthread_equal
  .weakref  _ZL20__gthrw_pthread_selfv,pthread_self
  .weakref  _ZL22__gthrw_pthread_detachm,pthread_detach
  .weakref  _ZL22__gthrw_pthread_cancelm,pthread_cancel
  .weakref  _ZL19__gthrw_sched_yieldv,sched_yield
  .weakref  _ZL26__gthrw_pthread_mutex_lockP15pthread_mutex_t,pthread_mutex_lock
  .weakref  _ZL29__gthrw_pthread_mutex_trylockP15pthread_mutex_t,pthread_mutex_trylock
  .weakref  _ZL31__gthrw_pthread_mutex_timedlockP15pthread_mutex_tPK8timespec,pthread_mutex_timedlock
  .weakref  _ZL28__gthrw_pthread_mutex_unlockP15pthread_mutex_t,pthread_mutex_unlock
  .weakref  _ZL26__gthrw_pthread_mutex_initP15pthread_mutex_tPK19pthread_mutexattr_t,pthread_mutex_init
  .weakref  _ZL29__gthrw_pthread_mutex_destroyP15pthread_mutex_t,pthread_mutex_destroy
  .weakref  _ZL30__gthrw_pthread_cond_broadcastP14pthread_cond_t,pthread_cond_broadcast
  .weakref  _ZL27__gthrw_pthread_cond_signalP14pthread_cond_t,pthread_cond_signal
  .weakref  _ZL25__gthrw_pthread_cond_waitP14pthread_cond_tP15pthread_mutex_t,pthread_cond_wait
  .weakref  _ZL30__gthrw_pthread_cond_timedwaitP14pthread_cond_tP15pthread_mutex_tPK8timespec,pthread_cond_timedwait
  .weakref  _ZL28__gthrw_pthread_cond_destroyP14pthread_cond_t,pthread_cond_destroy
  .weakref  _ZL26__gthrw_pthread_key_createPjPFvPvE,pthread_key_create
  .weakref  _ZL26__gthrw_pthread_key_deletej,pthread_key_delete
  .weakref  _ZL30__gthrw_pthread_mutexattr_initP19pthread_mutexattr_t,pthread_mutexattr_init
  .weakref  _ZL33__gthrw_pthread_mutexattr_settypeP19pthread_mutexattr_ti,pthread_mutexattr_settype
  .weakref  _ZL33__gthrw_pthread_mutexattr_destroyP19pthread_mutexattr_t,pthread_mutexattr_destroy

Por que o g++ colocou essas referêncas? Acontece que a STL prevê o uso de threads. Como ela tem que ser genérica, a biblioteca faz um monte de testes e tenta manter a sincronia de threads (mesmo num ambiente single threaded! De outro modo, porque teria tantas referências às funções da libpthread?). Tá certo que são referências “fracas”, que podem ser extirpadas pelo linker, mas isso implica que a STL tem uma complexidade tal que a torna quase imprevisível, do ponto de vista interno. Eu chuto de uns 50 a 200 ciclos de máquina para o construtor do objeto basic_string. Isso é de 10 a 40 vezes mais lento que o equivalente em C.

As strings ‘s1’ e ‘s2’ passam pelo mesmo tipo de inicialização (sem cópias, inicialmente). Mas, toda atribução de strings é feita com cópias. Se você fizer algo assim:

string s;
string s2;

s = "fred";
s2 = s;

Adivinhe só com quantas cópias da sequência de bytes “fred” [e o byte zero adicional] você terminará… É só contar: 3 cópias!

E, falando em cópias, strchr(), na rotina em C não faz “cópias”. Ele só varre o buffer e retorna o ponteiro de onde achou o token.

No caso de C++, sempre que uma cópia de strings é feita é necessário realizar alguma alocação dinâmica. Note que não há nenhuma verificação no código C++ além do resultado da função-membro find(). O seu programa não verifica se o objeto consguiu ou não alocar a memória necessária para conter a cópia! E a verificação desses erros, se feitas, não estão diretamente disponíveis via chamadas de funções da STL, mas através do mecanismo de tratamento de exceções… Assim, para garantir que seu código funcione sem problemas, você poderia fazer algo assim:

  try {
    string s = "frederico.pissarra";
  } 
  catch (bad_alloc& e) {
    // trata erro aqui!
  }

O que torna seu código mais “macarrônico”, mais pesado e (obviamente) pior que o equivalente em C (porque o compilador gerará mais um batalhão de código “escondido”).

Nosso código em C modifica o conteúdo do buffer original. Se quiséssemos fazer uma cópia de trabalho do buffer, nada seria mais simples do que fazer:

  char *tmp = strdup(s);  /* É assim que se faz uma cópia de strings, em C. */
  if (tmp != NULL)
  {
    ... /* trabalhando com 'tmp' como se fosse 's' aqui... */

    free(tmp); /* joga a cópia no lixo! */
  }
  else
  {
    /* tratamento de erro de alocação aqui. */
  }

Olha ai algo “parecido” com o try..catch. Só não é igual porque não tem código maluco escondido.

Os tiros de misericórdia são as comparações de tamanho e velocidade:

$ gcc -O3 -s test.c -o test
$ g++ -O3 -s test++.cxx -o test++
$ ls -l
total 24
-rwxrwxr-x 1 fred fred 6224 Apr 13 00:51 test
-rwxrwxr-x 1 fred fred 6448 Apr 13 00:51 test++
-rw-rw-r-- 1 fred fred  268 Apr 13 00:51 test.c
-rw-rw-r-- 1 fred fred  371 Apr 13 00:41 test++.cxx

Somente 224 bytes de diferença. Grandes coisas! Acredite-me: 224 bytes de código é MUITA coisa num programinha tão pequeno. Adicione centenas de rotinas e objetos e a diferença poderá chegar na ordem de grandeza de alguns megabytes.

E, pelos meus testes, no fim das contas, esse simples programinha em C++ é cerca de 4 vezes mais lento que o mesmo programinha em C.

Continuo preferindo C… C++, além de gerar código mais pesado, é mais complicado e tira o controle do meu código de minhas mãos.

Anúncios

2 comentários sobre “Qual dos dois você prefere?

  1. Fred,

    Tenho lido pouco estes seus artigos e quase comentei o útlimo, quando
    iria sugerir o par try/catch ao invés daquele goto “exótico”. Mas
    desisti, pois ando tão afastado de programação em “C” que preferi não
    soltar outra “asneira”. Mas hoje não pude resistir, ao ver você
    citando – lógico que não está recomendando – como alternativa no
    codigo o uso do try/catch.

    Ok, primeiro sobre o try/catch: a meu ver é muito usado pois melhora a
    legibilidade, mas tem a contra partida de colocar tudo num saco só e
    depois separar o joio do trigo ao tratar a exceção. A contra partida é
    a questão que permanece: será que vale à pena sacrificar tanto o
    processamento em nome da legibilidade?

    Segundo, sobre os threads: é claro que quando vocẽ compra um canivete
    suiço com trocentas lâminas você não imagina que pode fazer muito mais
    do que descascar laranjas e esnobá-lo para os amigos. Com o tempo vai
    descobrindo – e um belo dia voce percebe que o saca-rolhas do canivete
    também funciona. O mesmo se dá com os atuais phoblets.

    Enfim um argumento que vejo bastante nas minhas andanças é que um
    programador “C” trata sim cada caso, caso a caso, pois é a única forma
    de se ter certeza que você previu tudo e seu código é failproof. Mas
    sejamos honestos: é bem difícil, quiçá impossível e frustrante prever
    tudo. Seu código fica imenso e bem difícil de se ser analisado e
    mantido. Ora se usarmos estes recursos modernosos como try/catch
    pegamos atalhos e chegamos mais rápido… Para depois gastar o tempo
    economizado para corrigir as cagadas que ficaram para trás e não foram
    vistas. Por outro lado tempo é dinheiro e tempo de programador de meia
    pataca é barato. Bota meia dúzia de vebero, delfero, javero, phpero
    para resolver parada e bota os “matusaléns do art-code” para bordar a
    toalha.

    Voltando, não tenho dúvida de que o código ficará muito mais confiável
    otimizado se você tratá-lo como artefatos aero-espaciais mas sabemos
    que depois de embalado e colocado nas prateleiras para um determinado
    core, daqui a 6 meses estará obsoleto e irá requerer manutenção,
    ajustes… E nem sempre estaremos vivos ou mesmo dispostos a arregaçar
    as mangas e começar tudo de novo. Corta, entra em cenas os veberos,
    delpheros, javeros… Faturando. E os “C” smiths, sentados nas bancas
    acadêmicas de elaboração de provas das quase-sem-fundos universidades,
    formando… Delferos, Veberos, Javeros. Tentando incutir-lhes a idéia
    que a verdadeira “arte de programar” está nos mofados livros dos
    sebos.

    Então surge o dilema – quem irá fazer a manutenção, os veberos,…? Ou
    pior, quem poderá reescreves os “alfarrábios dos matusalens”? Ferrou
    pois os matusalens podem ainda estar ocupados bordando as toalhas
    antigas!

    Acho eu que usar o “C” para mastigar strings é uma falta de respeito
    com o potencial da linguagem. Lógico que no fim das contas que vai
    mastigar mesmo são os binários – muito bem escritos e imutáveis
    códigos – escritos em assembly pelo autor do compilador usando “C” de
    uma biblioteca “wrapada” e comandados por um destes new kids of the
    block, – Python, javascript, php, perl, ruby, etc. Que farão um
    trabalho – bem distante da camada “inteligente” – modelados e
    implandados por administradores e administrados “sem tempo para
    escovação de bits”. Afinal é para isto que as pessoas já se
    acostumaram – bem bovinadamente educadas pela m$ – a engolir patches,
    remendos nas toalhas e até mesmo trocas abrutas de paradigmas – sob a
    obscura capa de “atualizações”. Vai saber que que estão empurrando na
    goela do seu computador… Dane-se, desde que o faceburruk rode, tá
    bom!

    Então para responder a pergunta proposta pelo post, eu digo que não
    prefiro nenhum dos dois, mas sim o Python que entendeu isto muito bem,
    se afastou do java, do c/c++ e passou a se comunicar bem com todos –
    os veberos, delpheros, javeros – incorporando a pegada do c/c++ usando
    muito bem a modularização, a programação funcional ou se você quiser
    ele ainda manda bem nas classes – se você não for muito exigente
    quanto a polimorfismo.

    É claro que ter um código bem feito e o conhecimento de como fazê-lo,
    nunca irá perder o valor e a importância – mas é um corte muito nobre
    para ser feito por “picadores de gelo” como eu e a apreciação deste
    tipo de trabalho tem um universo bem seleto de paladares.

    Por fim, acho muito interessante saber usar os recursos dentro de seus
    domínios e a conclusão que cheguei nestes últimos meses, nos quais
    aprendi a programar usando o Python. Faça o protótipo usando o Python
    e nos trechos “nobres” pegue um desvio para o “C”. Combinemos que até
    hoje ainda não precisei pegar o desvio e estou muito feliz com os
    resultados e a robustez do Python. Mas ainda lembro bem de como eu
    reagi quando você me sugeriu usá-lo, há uns 8 anos atrás: – Prrra este
    negócio de tabizinho funcionando como sintaxe é coisa de mané!

    Como vê é tudo uma questão de objetivos. Os meus diferem quanto aos
    seus em termos de resultados e urgências, mas coadunam fortemente
    quando se trata em manter a boa e velha tradição ao se programar: faça
    o certo e use o mímimo de recursos possíveis.

    1. Primeiro… eu não “recomendei” o uso de try catch… fiz justamente o contrário. Estou condenando C++ aqui.

      Frameworks do tipo “canivete suíço” não me agradam nem um pouco…

      Sempre fui um adepto a não usar recursos facilitadores demais, que entregam o controle do seu código nas mãos de frameworks e facilidades escondidas (e porcamente documentadas – vai me dizer que sabe EXATAMENTE quando usar referências ou não num “catch” e quando usar essa ou aquela classe de exceções?). Da mesma forma que a exceção certa precisa ser “pagada”, paa o tratamento correto, é necessário entender a semântica das funções da biblioteca padrão que está usando. A primeira é bem documentada nas man pages (experimente instalar libstdc++6-4.6-doc e consultar as man pages da STL, por exemplo).

      O interessante sobre os “mathusalens do art-code” é que eles nos dão a maioria dos códigos sérios e estáveis do Linux, por exemplo. Apache (em C, não C++), ffmpeg (C), GIMP (C), GNOME (C) etc… É raro ver algo realmente sério em C++. Aliás, considere que os recursos “facilitadores”, como a STL, tem zilhões de poréns (veja os livros de Scott Meyers: Effective C++ com 50 dicas sobre STL [cada uma mostrando que a forma “intuitiva” dá merda!]).

      Sobre obsolecência de código: Todo código fica obsoleto. Por isso inventaram um treco chamado “versão”. O Apache, por exemplo, já está na versão 2.3 – mas AINDA é escrito em C.

      E eu não estou falando de script languages aqui… Se é para falar de scripts e preferências a coisa fica muito mais complicada… Falemos então a lingua do “P”: PERL, PHP. Até Postscript é uma linguagem script (usada para impressão, mas pode ser usada para uso geral, sabia?). Estou falando especificamente das diferenças entre C e C++.

      Java e .NET, por exemplo, também não me agradam… especialmente porque já vi bugs de cabelo em códigos relativamente simples que não deveriam existir se a filosofia do canivete suíço fosse realmente conveniente. E Java ainda tem outro problema: Cada versão nova da J2SE invalida coisas importantes da versão anterior… Um código feito para J2SE 6 não funciona direito, na maioria das vezes, na J2SE 7 (ou, a mais recente, a 8). Isso destrói o argumento de que “facilidades” mantém o código mais fresquinho por mais tempo.

      Eu também não estou falando de “produtividade”. Essa idéia de que “tempo é dinheiro” é coisa de nêgo rico que não quer perder dinheiro. O desenvolvimento rápido, usando “facilidades” é, penso eu, a principal causa de perda de dinheiro: Veja o histórico da maioria das empresas de desenvolvimento “rápido”… Elas “fazem dinheiro” vendendo seu produto recém sídos do forno, mas depois têm que gastar 10 vezes mais tempo corrigindo problemas não previstos (por PREGUIÇA!). Acabam fechando as portas no fim das contas ou permanecem lutando para não fecharem (já trabalhei em algumas delas).

      Eu também não disse que vou desenvolver meus códigos “bare metal”. Uso a libc, sim… mas, porque entendo o que a maioria das funções faz, por baixo dos panos e sei que, seguindo as “guidelines” da documentação (farta!), ela é muito confiável. O mesmo não se pode dizer de C++ (de novo… procure nas man pages!).

      Ainda vou escrever um post sobre minha maneira de aproximar-me do desenvolvimento de novas rotinas, mas ai vai um preview:

      1. Meus códigos geralmnte são organizados numa árvore de diretórios:

      ./
      ./src/
      ./src/include/
      ./sandbox/
      ./sandbox/tests/

      2. Sempre uso assert() em minhas funções;

      3. Em ./sandbox/tests/ crio minhas novas funções sobre um template:

      #include


      /* Minha função entra aqui */
      … minha_funcao(…) { … }

      int main(int argc, char *argv[])
      {
      /* prepara_parâmetros */

      /* chama função */
      minha_funcao(…);

      return 0;
      }

      Assim eu a depuro com o gdb num ambiente mais simples… A não ser que ela seja MUITO simples e não exija isso.

      4. Mesmo que eu saiba como uma função funcione SEMPRE consulto as man pages para ver se não tem algum porém que estou deixando passar.

      As ditas “facilidades” de linguagens script e outras OOP, acho, são enganosas. Mostrei que o código simples, em C++, tem muitos problemas escondidos. A ausência explícita do tratamento de exceções quando o que se está fazendo, por debaixo dos panos, é alocação dinâmica de memória, é um deles… E se colocá-los, o código ficará tão “macarrônico” quanto ficaria um código em C.

      É claro, somente alguma experiência (que eu e você temos) dá a capacidade de desenvolver códgo mais estável…

Deixe um comentário

Faça o login usando um destes métodos para comentar:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s