Por que as linhas de leitura do stdin são muito mais lentas em C ++ que no Python?


1840

Eu queria comparar as linhas de leitura de entrada de string do stdin usando Python e C ++ e fiquei chocado ao ver meu código C ++ executar uma ordem de magnitude mais lenta que o código Python equivalente. Como meu C ++ está enferrujado e ainda não sou um especialista em Python, me diga se estou fazendo algo errado ou se estou entendendo errado.


(Resposta do TLDR: inclua a declaração: cin.sync_with_stdio(false)ou use apenas fgets.

Resultados do TLDR: role até o final da minha pergunta e veja a tabela.)


Código C ++:

#include <iostream>
#include <time.h>

using namespace std;

int main() {
    string input_line;
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    while (cin) {
        getline(cin, input_line);
        if (!cin.eof())
            line_count++;
    };

    sec = (int) time(NULL) - start;
    cerr << "Read " << line_count << " lines in " << sec << " seconds.";
    if (sec > 0) {
        lps = line_count / sec;
        cerr << " LPS: " << lps << endl;
    } else
        cerr << endl;
    return 0;
}

// Compiled with:
// g++ -O3 -o readline_test_cpp foo.cpp

Equivalente em Python:

#!/usr/bin/env python
import time
import sys

count = 0
start = time.time()

for line in  sys.stdin:
    count += 1

delta_sec = int(time.time() - start_time)
if delta_sec >= 0:
    lines_per_sec = int(round(count/delta_sec))
    print("Read {0} lines in {1} seconds. LPS: {2}".format(count, delta_sec,
       lines_per_sec))

Aqui estão os meus resultados:

$ cat test_lines | ./readline_test_cpp
Read 5570000 lines in 9 seconds. LPS: 618889

$cat test_lines | ./readline_test.py
Read 5570000 lines in 1 seconds. LPS: 5570000

Devo observar que tentei isso tanto no Mac OS X v10.6.8 (Snow Leopard) quanto no Linux 2.6.32 (Red Hat Linux 6.2). O primeiro é um MacBook Pro e o último é um servidor muito robusto, não que isso seja pertinente demais.

$ for i in {1..5}; do echo "Test run $i at `date`"; echo -n "CPP:"; cat test_lines | ./readline_test_cpp ; echo -n "Python:"; cat test_lines | ./readline_test.py ; done
Test run 1 at Mon Feb 20 21:29:28 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 2 at Mon Feb 20 21:29:39 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 3 at Mon Feb 20 21:29:50 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 4 at Mon Feb 20 21:30:01 EST 2012
CPP:   Read 5570001 lines in 9 seconds. LPS: 618889
Python:Read 5570000 lines in 1 seconds. LPS: 5570000
Test run 5 at Mon Feb 20 21:30:11 EST 2012
CPP:   Read 5570001 lines in 10 seconds. LPS: 557000
Python:Read 5570000 lines in  1 seconds. LPS: 5570000

Minúsculo adendo e resumo

Para garantir a integridade, pensei em atualizar a velocidade de leitura do mesmo arquivo na mesma caixa com o código C ++ original (sincronizado). Novamente, isso é para um arquivo de linha de 100 milhões em um disco rápido. Aqui está a comparação, com várias soluções / abordagens:

Implementation      Lines per second
python (default)           3,571,428
cin (default/naive)          819,672
cin (no sync)             12,500,000
fgets                     14,285,714
wc (not fair comparison)  54,644,808

14
Você executou seus testes várias vezes? Talvez haja um problema de cache de disco.
Vaughn Cato

9
@JJC: Eu vejo duas possibilidades (supondo que você tenha removido o problema de armazenamento em cache sugerido por David): 1) o <iostream>desempenho é péssimo. Não é a primeira vez que isso acontece. 2) Python é inteligente o suficiente para não copiar os dados no loop for porque você não os usa. Você pode testar novamente tentando usar scanfe a char[]. Como alternativa, você pode tentar reescrever o loop para que algo seja feito com a string (por exemplo, mantenha a quinta letra e concatene-a em um resultado).
JN

15
O problema é a sincronização com o stdio - veja minha resposta.
Vaughn Cato

19
Como ninguém parece ter mencionado por que você recebe uma linha extra com C ++: Não teste contra cin.eof()!! Coloque a getlinechamada na instrução 'if'.
Xeo

21
wc -lé rápido porque lê o fluxo mais de uma linha por vez (pode ser uma fread(stdin)/memchr('\n')combinação). Resultados em Python estão na mesma ordem de magnitude, por exemplo,wc-l.py
jfs

Respostas:


1644

Por padrão, ciné sincronizado com o stdio, o que evita qualquer buffer de entrada. Se você adicionar isso à parte superior do seu principal, verá um desempenho muito melhor:

std::ios_base::sync_with_stdio(false);

Normalmente, quando um fluxo de entrada é armazenado em buffer, em vez de ler um caractere de cada vez, o fluxo será lido em partes maiores. Isso reduz o número de chamadas do sistema, que geralmente são relativamente caras. No entanto, uma vez que a FILE*base stdioeiostreams implementações geralmente têm separações e, portanto, buffers separados, isso pode levar a um problema se ambas forem usadas juntas. Por exemplo:

int myvalue1;
cin >> myvalue1;
int myvalue2;
scanf("%d",&myvalue2);

Se mais entradas foram lidas cindo que realmente eram necessárias, o segundo valor inteiro não estaria disponível para a scanffunção, que possui seu próprio buffer independente. Isso levaria a resultados inesperados.

Para evitar isso, por padrão, os fluxos são sincronizados com stdio. Uma maneira comum de conseguir isso é ter cinlido cada caractere um de cada vez, conforme necessário, usando stdiofunções. Infelizmente, isso introduz muita sobrecarga. Para pequenas quantidades de entrada, isso não é um grande problema, mas quando você está lendo milhões de linhas, a penalidade de desempenho é significativa.

Felizmente, os designers da biblioteca decidiram que você também deveria desabilitar esse recurso para obter um desempenho aprimorado, se soubesse o que estava fazendo, para que eles fornecessem o sync_with_stdiométodo.


142
Isso deve estar no topo. É quase certamente correto. A resposta não pode estar na substituição da leitura por uma fscanfchamada, porque isso simplesmente não funciona tanto quanto o Python. O Python deve alocar memória para a string, possivelmente várias vezes, pois a alocação existente é considerada inadequada - exatamente como a abordagem C ++ std::string. Essa tarefa é quase certamente vinculada à E / S e há muito FUD por aí sobre o custo de criar std::stringobjetos em C ++ ou usar <iostream>por si só.
Karl Knechtel

51
Sim, adicionar essa linha imediatamente acima do meu loop while original acelerou o código para superar até o python. Estou prestes a publicar os resultados como a edição final. Obrigado novamente!
21412 JJC

6
Sim, isso também se aplica ao cout, cerr e tamanco.
Vaughn Cato

2
Para tornar cout, cin, cerr e entupir mais rápido, faça o seguinte: std :: ios_base :: sync_with_stdio (false);
01100110

56
Observe que sync_with_stdio()é uma função membro estática e uma chamada para essa função em qualquer objeto de fluxo (por exemplo cin) ativa ou desativa a sincronização de todos os objetos iostream padrão.
John Zwinck

171

Só por curiosidade, dei uma olhada no que acontece sob o capô e usei dtruss / strace em cada teste.

C ++

./a.out < in
Saw 6512403 lines in 8 seconds.  Crunch speed: 814050

syscalls sudo dtruss -c ./a.out < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            6
pread                                           8
mprotect                                       17
mmap                                           22
stat64                                         30
read_nocancel                               25958

Pitão

./a.py < in
Read 6512402 lines in 1 seconds. LPS: 6512402

syscalls sudo dtruss -c ./a.py < in

CALL                                        COUNT
__mac_syscall                                   1
<snip>
open                                            5
pread                                           8
mprotect                                       17
mmap                                           21
stat64                                         29

159

Estou aqui há alguns anos, mas:

Em 'Editar 4/5/6' da postagem original, você está usando a construção:

$ /usr/bin/time cat big_file | program_to_benchmark

Isso está errado de duas maneiras diferentes:

  1. Na verdade, você está cronometrando a execução cat, não sua referência. O uso da CPU 'user' e 'sys' exibido por timesão aqueles que catnão são o seu programa de benchmarking. Pior ainda, o tempo 'real' também não é necessariamente preciso. Dependendo da implementação cate dos pipelines no sistema operacional local, é possível que ele catgrave um buffer gigante final e saia muito antes do processo do leitor concluir seu trabalho.

  2. O uso de caté desnecessário e de fato contraproducente; você está adicionando peças móveis. Se você estivesse em um sistema suficientemente antigo (ou seja, com uma única CPU e - em algumas gerações de computadores - E / S mais rápida que a CPU) - o simples fato de catestar em execução poderia colorir substancialmente os resultados. Você também está sujeito a qualquer buffer de entrada e saída e outro processamento que catpossa fazer. (Isso provavelmente lhe renderá um prêmio 'Uso inútil de gato' se eu fosse Randal Schwartz.

Uma construção melhor seria:

$ /usr/bin/time program_to_benchmark < big_file

Nesta declaração, é o shell que abre o big_file, passando-o para o seu programa (bem, na verdade, para o timequal o programa é executado como um subprocesso) como um descritor de arquivo já aberto. 100% da leitura do arquivo é estritamente de responsabilidade do programa que você está tentando comparar. Isso permite uma leitura real de seu desempenho, sem complicações espúrias.

Mencionarei duas 'correções' possíveis, mas realmente erradas, que também podem ser consideradas (mas as 'numero' de maneira diferente, pois essas não são coisas erradas no post original):

R. Você pode 'consertar' isso cronometrando apenas seu programa:

$ cat big_file | /usr/bin/time program_to_benchmark

B. ou cronometrando todo o pipeline:

$ /usr/bin/time sh -c 'cat big_file | program_to_benchmark'

Eles estão errados pelos mesmos motivos do item 2: eles ainda estão sendo usados catdesnecessariamente. Eu os mencionei por alguns motivos:

  • eles são mais "naturais" para pessoas que não estão totalmente confortáveis ​​com os recursos de redirecionamento de E / S do shell POSIX

  • pode haver casos em que cat é necessário (por exemplo: o arquivo a ser lido requer algum tipo de privilégio de acesso, e você não deseja conceder esse privilégio para o programa a ser aferido: sudo cat /dev/sda | /usr/bin/time my_compression_test --no-output)

  • na prática , em máquinas modernas, o acréscimo catno pipeline provavelmente não tem nenhuma consequência real.

Mas digo a última coisa com alguma hesitação. Se examinarmos o último resultado em 'Editar 5' -

$ /usr/bin/time cat temp_big_file | wc -l
0.01user 1.34system 0:01.83elapsed 74%CPU ...

- isso afirma que catconsumiu 74% da CPU durante o teste; e de fato 1,34 / 1,83 é de aproximadamente 74%. Talvez uma série de:

$ /usr/bin/time wc -l < temp_big_file

levaria apenas os restantes 0,49 segundos! Provavelmente não: cataqui tinha que pagar pelas read()chamadas do sistema (ou equivalente) que transferiam o arquivo do 'disco' (na verdade, cache de buffer), bem como o pipe grava para entregá-las wc. O teste correto ainda teria que fazer aquelesread() chamadas; apenas as chamadas de gravação para pipe e leitura de pipe seriam salvas, e essas devem ser bem baratas.

Ainda assim, eu prevejo que você seria capaz de medir a diferença entre cat file | wc -le wc -l < filee encontrar um (percentagem de 2 dígitos) diferença notável. Cada um dos testes mais lentos terá pago uma penalidade semelhante em tempo absoluto; o que equivaleria a uma fração menor do seu tempo total maior.

Na verdade, eu fiz alguns testes rápidos com um arquivo de lixo de 1,5 gigabyte, em um sistema Linux 3.13 (Ubuntu 14.04), obtendo esses resultados (esses são realmente os melhores resultados de 3); depois de preparar o cache, é claro):

$ time wc -l < /tmp/junk
real 0.280s user 0.156s sys 0.124s (total cpu 0.280s)
$ time cat /tmp/junk | wc -l
real 0.407s user 0.157s sys 0.618s (total cpu 0.775s)
$ time sh -c 'cat /tmp/junk | wc -l'
real 0.411s user 0.118s sys 0.660s (total cpu 0.778s)

Observe que os dois resultados do pipeline afirmam ter demorado mais tempo da CPU (usuário + sys) do que o tempo real do relógio de parede. Isso ocorre porque estou usando o comando 'time' interno do shell (bash), que é conhecedor do pipeline; e estou em uma máquina com vários núcleos, em que processos separados em um pipeline podem usar núcleos separados, acumulando o tempo da CPU mais rapidamente do que em tempo real. Usando /usr/bin/timevejo um tempo de CPU menor que o tempo real - mostrando que ele só pode cronometrar o único elemento do pipeline passado para ele em sua linha de comando. Além disso, a saída do shell fornece milissegundos, enquanto /usr/bin/timeapenas centésimos de segundo.

Portanto, no nível de eficiência de wc -l, isso catfaz uma enorme diferença: 409/283 = 1,453 ou 45,3% mais em tempo real e 775/280 = 2,768, ou uma CPU 177% maior usada! Na minha caixa de teste aleatória, estava lá na hora.

Devo acrescentar que há pelo menos uma outra diferença significativa entre esses estilos de teste e não posso dizer se é um benefício ou falha; você tem que decidir isso sozinho:

Quando você executa cat big_file | /usr/bin/time my_program, seu programa está recebendo informações de um canal, exatamente no ritmo enviado por cate em partes não maiores que as escritas por cat.

Quando você executa /usr/bin/time my_program < big_file, seu programa recebe um descritor de arquivo aberto para o arquivo real. Seu programa - ou em muitos casos as bibliotecas de E / S do idioma em que foi gravado - pode executar ações diferentes quando apresentado a um descritor de arquivo que faz referência a um arquivo regular. Pode ser usado mmap(2)para mapear o arquivo de entrada em seu espaço de endereço, em vez de usar read(2)chamadas explícitas do sistema. Essas diferenças podem ter um efeito muito maior nos resultados de benchmark do que o pequeno custo de execução do catbinário.

Obviamente, é um resultado de referência interessante se o mesmo programa tiver um desempenho significativamente diferente entre os dois casos. Isso mostra que, de fato, o programa ou suas bibliotecas de E / S estão fazendo algo interessante, como usar mmap(). Portanto, na prática, pode ser bom executar os benchmarks nos dois sentidos; talvez descontando o catresultado por algum pequeno fator para "perdoar" o custo da catprópria execução .


26
Uau, isso foi bastante esclarecedor! Embora eu esteja ciente de que o gato não é necessário para alimentar a entrada de programas e que o <redirecionamento de shell é o preferido, geralmente eu continuo preso ao gato devido ao fluxo de dados da esquerda para a direita que o método anterior preserva visualmente quando eu raciocino sobre gasodutos. As diferenças de desempenho nesses casos são insignificantes. Mas agradeço por nos educar, Bela.
JJC

11
O redirecionamento é analisado fora da linha de comando do shell em um estágio inicial, o que permite que você faça um deles, se houver uma aparência mais agradável do fluxo da esquerda para a direita: $ < big_file time my_program $ time < big_file my_program Isso deve funcionar em qualquer shell POSIX (ou seja, `` csh `e não tenho certeza sobre exóticas como` rc`:)
Bela Lubkin

6
Novamente, além da diferença de desempenho incremental talvez desinteressante devido à execução binária do `cat` ao mesmo tempo, você está desistindo da possibilidade do programa em teste ser capaz de mapear () o arquivo de entrada. Isso pode fazer uma profunda diferença nos resultados. Isso é verdade mesmo se você mesmo tiver escrito os benchmarks, nos vários idiomas, usando apenas o idioma 'linhas de entrada de um arquivo'. Depende do funcionamento detalhado de suas várias bibliotecas de E / S.
Bela Lubkin

2
Nota lateral: O built-in do Bash timeestá medindo todo o pipeline em vez do primeiro programa. time seq 2 | while read; do sleep 1; doneimprime 2 segundos, /usr/bin/time seq 2 | while read; do sleep 1; doneimprime 0 seg.
24918 folkol

1
@folkol - yes, << Observe que os dois resultados do pipeline [mostram] mais CPU [do que] em tempo real [usando] o comando interno 'time' do Bash (Bash); ... / usr / bin / time ... só pode cronometrar o único elemento do pipeline passado a ele em sua linha de comando. >> '
Bela Lubkin

90

Reproduzi o resultado original no meu computador usando o g ++ em um Mac.

Adicionando as seguintes instruções à versão C ++, pouco antes do whileloop, alinhar com a versão Python :

std::ios_base::sync_with_stdio(false);
char buffer[1048576];
std::cin.rdbuf()->pubsetbuf(buffer, sizeof(buffer));

O sync_with_stdio aumentou a velocidade para 2 segundos e a configuração de um buffer maior reduziu para 1 segundo.


5
Convém tentar tamanhos de buffer diferentes para obter informações mais úteis. Eu suspeito que você verá retornos rapidamente decrescentes.
21412 Karl Knechtel

8
Eu estava muito apressado em minha resposta; definir o tamanho do buffer para algo diferente do padrão não produziu uma diferença apreciável.
22412 karunski

109
Também evitaria configurar um buffer de 1 MB na pilha. Ela pode levar a stackoverflow (embora eu acho que é um bom lugar para debater sobre isso!)
Matthieu M.

11
Por outro lado, o Mac usa uma pilha de processos de 8 MB por padrão. O Linux usa 4 MB por thread padrão, IIRC. 1 MB não é um problema para um programa que transforma entrada com profundidade de pilha relativamente rasa. Mais importante, porém, std :: cin jogará a pilha no lixo se o buffer ficar fora do escopo.
SEK

22
@SEK O tamanho padrão da pilha do Windows é de 1 MB.
Étienne

39

getlineoperadores de stream, scanf pode ser conveniente se você não se importar com o tempo de carregamento do arquivo ou se estiver carregando pequenos arquivos de texto. Mas, se o desempenho é algo com o qual você se preocupa, você realmente deve colocar o arquivo inteiro na memória (supondo que ele caiba).

Aqui está um exemplo:

//open file in binary mode
std::fstream file( filename, std::ios::in|::std::ios::binary );
if( !file ) return NULL;

//read the size...
file.seekg(0, std::ios::end);
size_t length = (size_t)file.tellg();
file.seekg(0, std::ios::beg);

//read into memory buffer, then close it.
char *filebuf = new char[length+1];
file.read(filebuf, length);
filebuf[length] = '\0'; //make it null-terminated
file.close();

Se desejar, você pode agrupar um fluxo em torno desse buffer para um acesso mais conveniente como este:

std::istrstream header(&filebuf[0], length);

Além disso, se você estiver no controle do arquivo, considere usar um formato de dados binários simples em vez de texto. É mais confiável ler e escrever, porque você não precisa lidar com todas as ambiguidades do espaço em branco. Também é menor e muito mais rápido para analisar.


20

O código a seguir foi mais rápido do que o outro código postado aqui até agora: (Visual Studio 2013, arquivo de 500 bits e 64 MB com comprimento de linha uniforme em [0, 1000)].

const int buffer_size = 500 * 1024;  // Too large/small buffer is not good.
std::vector<char> buffer(buffer_size);
int size;
while ((size = fread(buffer.data(), sizeof(char), buffer_size, stdin)) > 0) {
    line_count += count_if(buffer.begin(), buffer.begin() + size, [](char ch) { return ch == '\n'; });
}

Ele supera todas as minhas tentativas de Python por mais de um fator 2.


Você pode ficar ainda mais rápido do que isso com um pequeno programa C personalizado, mas completamente direto, que iterativamente transforma readsyscalls sem buffer em um buffer estático de comprimento BUFSIZEou através dos mmapsyscalls correspondentes equivalentes e, em seguida, passa pelo buffer contando novas linhas à la for (char *cp = buf; *cp; cp++) count += *cp == "\n". Você precisará ajustar BUFSIZEo sistema, o que o stdio já terá feito por você. Mas esse forloop deve ser compilado para instruções em linguagem assembler incrivelmente rápidas e gritantes para o hardware da sua caixa.
tchrist

3
count_if e um lambda também são compilados para "um montador incrivelmente rápido".
Petter

17

A propósito, a razão pela qual a contagem de linhas para a versão C ++ é maior que a contagem para a versão Python é que o sinalizador eof só é definido quando é feita uma tentativa de ler além do eof. Portanto, o loop correto seria:

while (cin) {
    getline(cin, input_line);

    if (!cin.eof())
        line_count++;
};

70
O loop realmente correta seria: while (getline(cin, input_line)) line_count++;
Jonathan Wakely

2
@JonathanWakely eu sei que estou muito atrasado, mas uso ++line_count;e não line_count++;.
val diz Reintegrar Monica

7
@val, se isso fizer alguma diferença, seu compilador possui um bug. A variável é a long, e o compilador é capaz de dizer que o resultado do incremento não é usado. Se ele não gerar código idêntico para pós-incremento e pré-incremento, está quebrado.
Jonathan Wakely

2
De fato, qualquer compilador decente poderá detectar um uso indevido pós-incremento e substituí-lo por um pré-incremento, mas os compiladores não precisam . Portanto, não, ele não está quebrado, mesmo que o compilador não execute a substituição. Além disso, escrevendo ++line_count;em vez de line_count++;não ferir :)
Fareanor

1
@valsaysReinstateMonica Neste exemplo específico, por que um deles seria preferido? O resultado não é usado aqui de qualquer maneira, então seria lido depois do while, certo? Importaria se houvesse algum tipo de erro e você quisesse verificar se line_countestava correto? Estou apenas adivinhando, mas não entendo por que isso importaria.
TankorSmash

14

No seu segundo exemplo (com scanf ()), o motivo pelo qual isso ainda é mais lento pode ser porque o scanf ("% s") analisa a sequência e procura por qualquer caractere de espaço (espaço, guia, nova linha).

Além disso, sim, o CPython faz cache para evitar leituras de disco rígido.


12

Um primeiro elemento de uma resposta: <iostream>é lento. Muito lento. Recebo um enorme aumento de desempenho scanfcomo no exemplo abaixo, mas ainda é duas vezes mais lento que o Python.

#include <iostream>
#include <time.h>
#include <cstdio>

using namespace std;

int main() {
    char buffer[10000];
    long line_count = 0;
    time_t start = time(NULL);
    int sec;
    int lps;

    int read = 1;
    while(read > 0) {
        read = scanf("%s", buffer);
        line_count++;
    };
    sec = (int) time(NULL) - start;
    line_count--;
    cerr << "Saw " << line_count << " lines in " << sec << " seconds." ;
    if (sec > 0) {
        lps = line_count / sec;
        cerr << "  Crunch speed: " << lps << endl;
    } 
    else
        cerr << endl;
    return 0;
}

Não vi este post até fazer minha terceira edição, mas obrigado novamente por sua sugestão. Estranhamente, não há um acerto 2x para mim vs. python agora com a linha scanf na edição3 acima. Estou usando 2.7, por sinal.
JJC

10
Depois de corrigir a versão do c ++, essa versão do stdio é substancialmente mais lenta que a versão do c ++ iostreams no meu computador. (3 segundos versus 1 segundo)
karunski

10

Bem, eu vejo que na sua segunda solução você mudou cinpara scanf, qual foi a primeira sugestão que eu ia fazer (cin é sloooooooooooow). Agora, se você mudar de scanfpara fgets, verá outro aumento no desempenho:fgets é a função C ++ mais rápida para entrada de string.

BTW, não sabia sobre essa coisa de sincronização, legal. Mas você ainda deve tentar fgets.


2
Exceto fgetsque estará errado (em termos de contagem de linhas e em termos de divisão de linhas entre os loops, se você realmente precisar usá-las) para linhas suficientemente grandes, sem verificações adicionais para linhas incompletas (e tentar compensar isso envolve alocar buffers desnecessariamente grandes , onde std::getlinelida com a realocação para corresponder perfeitamente à entrada real). Rápido e errado é fácil, mas quase sempre vale a pena usar "um pouco mais devagar, mas correto", o que sync_with_stdioé desativado .
ShadowRanger
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.