Testes de desempenho
● haystack (palheiro): um array com
10_000_000 de floats distintos
● needles (agulhas): 1_000 floats, sendo 500
presentes em haystack, 500 ausentes
●
tarefa: procurar cada uma das 1_000 agulhas
no palheiro e contar as ocorrências
– a contagem será sempre 500
Slide 4
Slide 4 text
Código da tarefa
found = 0
for n in needles:
if n in haystack:
found += 1
Slide 5
Slide 5 text
Resultados com listas de 5 tamanhos
len(haysack) fator tempo* fator
1_000 1× 9.12ms 1.00×
10_000 10× 78.22ms 8.58×
100_000 100× 767.98ms 84.25×
1_000_000 1_000× 8_020.31ms 879.90×
10_000_000 10_000× 78_558.78ms 8_618.63×
* Tempos para buscar 1_000 floats em um laptop Core i7 2.2GHz
rodando Python 3.8.
Slide 6
Slide 6 text
Análise dos resultados com listas
●
Buscar 1_000 agulhas em uma lista de 1_000
itens levou 9ms; na lista de 10_000_000 itens
levou 78s
●
Com o palheiro 10_000 maior, o tempo cresceu
8_619×
Slide 7
Slide 7 text
Resultados com dicts de 5 tamanhos
len(haysack) fator tempo* fator
1_000 1× 0.10ms 1.00×
10_000 10× 0.11ms 1.10×
100_000 100× 0.16ms 1.58×
1_000_000 1_000× 0.37ms 3.76×
10_000_000 10_000× 0.52ms 5.17×
* Tempos para buscar 1_000 floats em um laptop Core i7 2.2GHz
rodando Python 3.8.
Slide 8
Slide 8 text
Análise dos resultados com dicts
●
Buscar 1_000 agulhas em um dict de 1_000
itens levou 100µs; no dict de 10_000_000 itens
levou 512µs (meio microssegundo por agulha)
●
Com o palheiro 10_000 maior, o tempo cresceu
pouco mais que 5×
Slide 9
Slide 9 text
Código da tarefa com & (sets ou dicts)
found = 0
for n in needles:
if n in haystack:
found += 1
found = len(needles & haystack)
produz o mesmo resultado que
Análise dos resultados com set &
●
Intersecção de set de 1_000 agulhas com set
de 1_000 itens levou 83µs; no set com
10_000_000 de itens levou 298µs (298
nanossegundos por agulha)
●
Com o palheiro 10_000 maior, o tempo cresceu
≈ 3.6×
Slide 12
Slide 12 text
Hashes e igualdade
Slide 13
Slide 13 text
Funções de hash
●
Um função de hash processa uma quantidade
arbitrária de dados e devolve uma sequência
de bits de tamanho fixo: o código hash ou
simplesmente hash
>>> bin(hash('A'))
'0b10010101011000011000101001110011100101011000110011011000100010'
>>> bin(hash('O tempo é o tecido da vida.-Antonio Candido'))
'0b10111100100011100101100110110100000101000111011101111100100100'
Slide 14
Slide 14 text
Funções de hash (2)
●
Uma boa função de hash usa um algoritmo de
dispersão para que o hash de dois objetos
parecidos sejam bastante diferentes,
otimizando o espalhamento dos resultados
>>> bin(hash('O tempo é o tecido da vida.-Antonio Candido'))
'0b10111100100011100101100110110100000101000111011101111100100100'
>>> bin(hash('O tempo é o tecido da vida. -Antonio Candido'))
'0b11011000001001011110010101011011100101000111010110111111010011'
Slide 15
Slide 15 text
Funções de hash em Python
●
A função hash() embutida no Python usa
diferentes algoritmos para diferentes tipos.
●
Para str e bytes, usa o algoritmo criptográfico
SipHash e um salt (constante aleatória) por
processo
●
Para int até a largura da palavra*, usa o próprio
int, exceto que hash(-1) é -2, porque o código
-1 é reservado (como veremos logo mais)
Slide 16
Slide 16 text
Hashes e igualdade
●
Para instâncias de classes de usuário, hash()
invoca o método __hash__ na instância
●
Se dois objetos tem hashes diferentes, seus
valores certamente são diferentes
– Você precisa garantir isso implementando
__hash__ e __eq__ se quiser criar objetos
hasheable (hasheáveis)
Slide 17
Slide 17 text
Colisão de hash
●
Por definição, podem existir objetos diferentes
com o mesmo hash
– Por exemplo: considere que há muito mais strings
possíveis do que hashes de 64 bits
●
Se dois objetos de valores diferentes tem o
mesmo hash, isso é uma colisão de hash
– É esperado que isso aconteça às vezes, mas é
difícil encontrar um exemplo real
Slide 18
Slide 18 text
Hashes e igualdade (2)
●
Se dois objetos de valores iguais tem hashes
diferentes, isso é um bug sério
– Dicionários e sets em Python não funcionam se
isso acontecer
– Já que 1 == 1.0, então hash(1) == hash(1.0)
Como é armazenado um set
Considere um set com 5 elementos:
Note: a ordem dos elementos não é preservada.
>>> workdays = {'Mon', 'Tue', 'Wed', 'Thu', 'Fri'}
>>> workdays
{'Tue', 'Mon', 'Wed', 'Fri', 'Thu'}
Slide 22
Slide 22 text
Um set e sua hash table
>>> workdays = {'Mon', 'Tue', 'Wed', 'Thu', 'Fri'}
hash table
com 8 baldes
(buckets)
ocupação
máxima: ⅔
Slide 23
Slide 23 text
Consulta: 'Mon' in weekdays
>>> day = 'Mon'
>>> h = hash(day)
>>> h
4199492796428269555
Slide 24
Slide 24 text
Consulta: 'Mon' in weekdays
>>> day = 'Mon'
>>> h = hash(day)
>>> h
4199492796428269555
>>> offset = h % len(htable)
>>> offset
3
Slide 25
Slide 25 text
Consulta: 'Mon' in weekdays
>>> day = 'Mon'
>>> h = hash(day)
>>> h
4199492796428269555
>>> offset = h % len(htable)
>>> offset
3
>>> htable[offset].code == h
True
✔
Slide 26
Slide 26 text
Consulta: 'Mon' in weekdays
>>> day = 'Mon'
>>> h = hash(day)
>>> h
4199492796428269555
>>> offset = h % len(htable)
>>> offset
3
>>> htable[offset].code == h
True
>>> htable[offset].value == day
True
✔
✔
resultado:
True
Slide 27
Slide 27 text
Consulta 2: 'Fri' in weekdays
>>> day = 'Fri'
>>> h = hash(day)
>>> h
7021641685991143771
Slide 28
Slide 28 text
Consulta 2: 'Fri' in weekdays
>>> day = 'Fri'
>>> h = hash(day)
>>> h
7021641685991143771
>>> offset = h % len(htable)
>>> offset
3
Slide 29
Slide 29 text
Consulta 2: 'Fri' in weekdays
>>> day = 'Fri'
>>> h = hash(day)
>>> h
7021641685991143771
>>> offset = h % len(htable)
>>> offset
3
>>> htable[offset].code == h
False
✖
colisão de índice
(index collision)
Slide 30
Slide 30 text
Consulta 2: 'Fri' in weekdays
>>> day = 'Fri'
>>> h = hash(day)
>>> h
7021641685991143771
>>> offset = h % len(htable)
>>> offset
3
>>> htable[offset].code == h
False
>>> offset += 1
>>> offset
4
>>> htable[offset].code == h
False
✖
✖
2ª colisão de índice
Consulta 3: 'Sat' in weekdays
>>> day = 'Sat'
>>> h = hash(day)
>>> h
6624772952138908622
>>> offset = h % len(htable)
>>> offset
6
>>> htable[offset].code = -1
True
balde vazio,
resultado:
False
⭕
Slide 34
Slide 34 text
Algoritmo para inserir elemento
Slide 35
Slide 35 text
Exemplos de inserção de elemento
>>> s = {1.0, 2.0, 3.0}
>>> s.add(4)
>>> s
{1.0, 2.0, 3.0, 4}
>>> s.add(1)
>>> s
???
Slide 36
Slide 36 text
Exemplos de inserção de elemento
>>> s = {1.0, 2.0, 3.0}
>>> s.add(4)
>>> s
{1.0, 2.0, 3.0, 4}
>>> s.add(1)
>>> s
{1.0, 2.0, 3.0, 4}
Slide 37
Slide 37 text
Consequências da implementação
●
Elementos tem que ser hasheable
– obrigatório implementar __hash__ e __eq__
●
Pesquisar é calcular hash, offset, e comparar
– tempo quase não depende do tamanho do set
●
A ordem não é preservada e pode mudar
– quando o set cresce, a hash table é refeita
●
Mínimo de ⅓ de espaço ocioso na hash table
Slide 38
Slide 38 text
dicts antigos (Python < 3.6)
Slide 39
Slide 39 text
Como era armazenado um dict
Considere um dict com 4 itens:
A ordem dos itens não era preservada.
students = {'Mon': 14, 'Tue': 12, 'Wed': 14, 'Thu': 11}
Slide 40
Slide 40 text
Um dict antigo e sua hash table
students = {'Mon': 14, 'Tue': 12, 'Wed': 14, 'Thu': 11}
Slide 41
Slide 41 text
Muito espaço ocioso na hash table
cada balde vazio ocupa 64 * 3 = 196 bytes
Slide 42
Slide 42 text
dicts compactos (Python ≥ 3.6)
Slide 43
Slide 43 text
Um dict compacto e suas tabelas
students = {'Mon': 14, 'Tue': 12, 'Wed': 14, 'Thu': 11}
Slide 44
Slide 44 text
Tabela de índices otimiza o espaço
a tabela de índices usa inteiros de 8 bits para dicts
com até 127 elementos, 16 bits até 32_767, etc.
Slide 45
Slide 45 text
Tabela de entradas é compacta
a tabela de entradas não tem baldes vazios,
e preserva a ordem de inserção dos itens!
Slide 46
Slide 46 text
Inserção de elementos
Calcula hash e offset (3); se índice é -1,
adiciona entrada e coloca índice 0 no offset 3
Slide 47
Slide 47 text
Inserção de elementos
Calcula hash e offset (2); se índice é -1,
adiciona entrada e coloca índice 1 no offset 2
Slide 48
Slide 48 text
Inserção de elementos
Calcula hash e offset (4); se índice é -1,
adiciona entrada e coloca índice 2 no offset 4
Slide 49
Slide 49 text
Inserção de elementos
Calcula hash e offset (7); se índice é -1,
adiciona entrada e coloca índice 3 no offset 7
Slide 50
Slide 50 text
Consequências da implementação
●
Chaves tem que ser hasheable
●
Pesquisar é calcular hash, offset, e comparar
– tempo quase não depende do tamanho do dict
●
A ordem de inserção é preservada
– quando o dict cresce, cada novo item é salvo no
final da tabela de entradas
●
Só há espaço ocioso na tabela de índices
Slide 51
Slide 51 text
finalmentes
Slide 52
Slide 52 text
PEP 412: Key-sharing dictionary
●
Uma otimização de espaço adicional usada
somente nos __dict__ de classes e
instâncias, onde Python armazena os atributos
●
Só funciona para os atributos criados no
__init__
●
Detalhes no PEP 412 e neste texto:
https://bit.ly/flupy-pep412
Slide 53
Slide 53 text
Uma invenção de Hans Peter Luhn
●
Imigrou da Alemanha para
os EUA antes dos nazistas
tomarem o poder
●
Trabalhou na IBM
●
Inventor prolífico (KWIC etc.)
●
Ótimo artigo de história na
revista IEEE Spectrum
https://bit.ly/ieee-hans-luhn