Dez mil registros, varredura completa. Quantas páginas tem o arquivo, e quantas leituras a varredura fez?
páginas do arquivo 21 // 1 de metadados, 20 de dados
leituras, versão com iterador 20
leituras, versão que relê a página 10000
E na segunda varredura, dentro do mesmo processo, os dois números se repetem inteiros. O programa já tinha lido tudo, e leu tudo de novo.
É esse desperdício que o M2 apaga. Hoje a gente entende o mecanismo, e quinta vocês implementam.
| onde está o dado | tempo real | em escala humana |
| Cache L1 | 1 ns | 1 s |
| Memória principal | 100 ns | 2 min |
| SSD NVMe | 100 µs | 27 h |
| Disco magnético | 10 ms | 116 dias |
A coluna da direita é a mesma escala, esticada como se o acesso a L1 levasse um segundo. Buscar uma página no SSD é um dia inteiro de espera, e no disco magnético são quatro meses.
O trabalho de um SGBD é, quase todo, evitar a terceira e a quarta linha desta tabela.
Uma região de memória que guarda cópias de páginas do arquivo, e um conjunto de regras para decidir quem entra, quem fica e quem sai.
O nome que aparece na literatura é buffer pool. É a estrutura mais quente de qualquer banco de dados.
O sistema operacional já tem um cache de páginas. Bastaria usar mmap e deixar ele resolver. Nenhum banco sério faz isso, por quatro motivos.
Leitura curta e direta sobre isso: Crotty, Leis e Pavlo, Are You Sure You Want to Use MMAP in Your DBMS?, CIDR 2022.
Posições de memória do tamanho de uma página. A capacidade do cache é o número de frames.
Mapeia número da página para o frame em que ela está. É por aqui que o cache responde se acertou.
Indexada pela página, nunca pelo frame
Diz se a cópia em memória difere da cópia no disco. Página limpa pode sair sem custo.
Quantos usuários estão com a página aberta neste instante. Página fixada não pode ser expulsa.
Três dicionários e um contador. O M2 é o módulo mais curto do semestre, e o que mais quebra os outros quando sai errado.
buf = cache.fixa(7) // achou no cache, ou leu do disco e fixou ... usa o buffer ... // enquanto está fixada, ninguém a expulsa cache.solta(7, sujou=True) // devolve, e avisa se alterou
try/finally. Em C, o unpin em todos os caminhos de saída, inclusive no de erroO iterador do M5 está no meio da página 7. O cache enche e escolhe expulsar a 7. O que acontece?
Página fixada é página intocável. Se todas estiverem fixadas e o cache encher, a resposta certa é falhar alto, e não expulsar assim mesmo.
Página suja é a que foi alterada em memória e ainda não foi gravada no disco.
kill -9O cache pode expulsar, e gravar no disco, uma página suja de uma transação que ainda não fez COMMIT.
Ganha memória, e exige saber desfazer
O COMMIT não obriga a gravar todas as páginas alteradas pela transação.
Ganha tempo, e exige saber refazer
O minidb vai ser steal e no-force, como quase todo banco real. As duas escolhas são feitas aqui, no M2, e a conta chega no M7: é exatamente por causa delas que o log precisa existir.
Sai quem entrou primeiro. Simples, e ignora completamente o uso.
Sai quem foi usado há mais tempo. Aposta que o passado recente prevê o futuro próximo.
É o que o M2 pede
Aproximação barata do LRU: um bit de referência por frame e um ponteiro que gira. Sem lista para reordenar a cada acesso.
CLOCK: ponteiro passa pelo frame
bit de referência 1 → zera o bit e segue // ganhou uma segunda chance
bit de referência 0 → expulsa este // e o ponteiro para aqui
Cache de 3 frames. A tabela tem 4 páginas, e a consulta varre a tabela inteira, duas vezes.
acessos 1 2 3 4 1 2 3 4
LRU F F F F F F F F // oito faltas em oito acessos
O nome disso é inundação por varredura. É o motivo de bancos reais usarem LRU-K, 2Q ou CLOCK com proteção de varredura.
No minidb, LRU basta. Mas eu quero que vocês saibam onde ele quebra, porque isso vai aparecer na medição de vocês.
Vinte páginas de dados, varredura completa rodada duas vezes no mesmo processo. Qual a taxa de acerto da segunda varredura?
cache com 16 frames → 0 acertos, 20 faltas // 0 por cento cache com 20 frames → 20 acertos, 0 faltas // 100 por cento
class Cache:
def __init__(self, pager, capacidade=16):
self.pager, self.cap = pager, capacidade
self.frames = {} # pagina -> buffer
self.suja = set()
self.fixada = {} # pagina -> contador
self.uso = OrderedDict() # ordem de uso, mais antigo primeiro
self.acertos = self.faltas = 0
def fixa(self, n):
if n in self.frames:
self.acertos += 1
self.uso.move_to_end(n)
else:
self.faltas += 1
if len(self.frames) == self.cap:
self._expulsa()
self.frames[n] = self.pager.le(n)
self.uso[n] = True
self.fixada[n] = self.fixada.get(n, 0) + 1
return self.frames[n]
def solta(self, n, sujou=False):
if sujou:
self.suja.add(n)
self.fixada[n] -= 1
def _expulsa(self):
for n in list(self.uso): # do mais antigo para o mais novo
if self.fixada.get(n, 0) == 0:
if n in self.suja:
self.pager.escreve(n, self.frames[n])
self.suja.discard(n)
del self.frames[n], self.uso[n]
return
raise RuntimeError("todas as páginas estão fixadas")
def descarrega(self): # no fim do programa, e no commit do M6
for n in list(self.suja):
self.pager.escreve(n, self.frames[n])
self.suja.clear()
self.pager.sync()
antes Tabela → Pagina → Pager → disco
depois Tabela → Pagina → Cache → Pager → disco
le ou escreve diretopager.le(n) por cache.fixa(n), e ganha o solta no fimEsse é o primeiro módulo que mexe em código já entregue. Todo módulo daqui para frente vai ser assim.
O dado some em silêncio, e só aparece em uma consulta muito depois. Grave antes de tirar o frame.
Se fixa devolve cópia do buffer, a alteração morre ali e o bit de sujeira não significa nada. Devolva a mesma referência.
soltaO cache enche de páginas fixadas, e a primeira expulsão falha. Pares de fixa e solta, sempre.
Uma única chamada direta ao Pager, e existem duas cópias da mesma página em memória, com valores diferentes.
Nenhuma dessas quebra os testes de aceitação de forma óbvia. Todas quebram o módulo seguinte.
fixaUsem capacidade pequena nos testes, três ou quatro frames. Com capacidade grande, a expulsão nunca acontece e o teste não prova nada.
NOTES.md com a decisão mais difícilNo NOTES.md deste módulo, digam qual capacidade vocês escolheram e por quê.
Antes de sair, o exercício de hoje
Cache com 3 frames e política LRU. A sequência de acessos é 1, 2, 3, 1, 4, 2, 5, 1, 2, 3.
Tragam a conta resolvida no papel para quinta. O código do M2 fica bem mais curto depois que essa sequência estiver clara.