Slide 1

Slide 1 text

Hash tables: como funcionam dicts e sets

Slide 2

Slide 2 text

Testes com listas, dicts, e sets

Slide 3

Slide 3 text

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

Slide 10

Slide 10 text

Resultados com sets de 5 tamanhos len(haysack) fator tempo* fator 1_000 1× 0.08ms 1.00× 10_000 10× 0.09ms 1.13× 100_000 100× 0.12ms 1.47× 1_000_000 1_000× 0.24ms 2.89× 10_000_000 10_000× 0.30ms 3.59× found = len(needles & haystack)

Slide 11

Slide 11 text

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)

Slide 19

Slide 19 text

Exemplos em CPython de 32 bits Valor Código hash 1 00000000000000000000000000000001 != 0 1.0 00000000000000000000000000000001 ------------------------------------------------ 1.0 00000000000000000000000000000001 ! !!! ! !! ! ! ! ! !! !!! != 16 1.0001 00101110101101010000101011011101 ------------------------------------------------ 1.0001 00101110101101010000101011011101 !!! !!!! !!!!! !!!!! !! ! != 20 1.0002 01011101011010100001010110111001 ------------------------------------------------ 1.0002 01011101011010100001010110111001 ! ! ! !!! ! ! !! ! ! ! !!!! != 17 1.0003 00001100000111110010000010010110

Slide 20

Slide 20 text

Sets e hash tables

Slide 21

Slide 21 text

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

Slide 31

Slide 31 text

Consulta 2: 'Fri' in weekdays ... >>> # continuando... >>> offset += 1 >>> offset 5 >>> htable[offset].code == h True ✖ ✖ ✔ ✔

Slide 32

Slide 32 text

Consulta 2: 'Fri' in weekdays ✖ ✖ ✔ ✔ ✔ ... >>> # continuando... >>> offset += 1 >>> offset 5 >>> htable[offset].code == h True >>> htable[offset].value == day True resultado: True

Slide 33

Slide 33 text

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