-
Notifications
You must be signed in to change notification settings - Fork 0
[GUIDE] Heap (priority queue)
Precisaremos de uma priority queue para ordenar os coders esperando para usar um dongle já ocupado. Em python isso é bastante fácil, temos bibliotecas como heapq que trazem implementações prontas de priority queue feitas com binary min-heap.
Mas, aqui, não podemos usar nada disso. Precisamos implementar a priority queue por conta pŕopria. A fila é importante para arbitrar qual coder esperando por um dongle irá recebe-lo quando for desocupado, usando FIFO ou EDF.
Sem isso a simulação até funciona, mas o burnout precoce é garantido sem uma lógica determinística que gerencie essa ordenação.
Binary heap é a estrutura de dados que usaremos para armazenar a fila num array de forma que inserir e retirar itens seja rápido. A regra das priority queues é que sempre que um nó é retirado ele deve ser o nó de maior prioridade da fila.
Para forçar essa regra, usaremos binary min-heap. Min-heap significa que o menor nó sempre estará no index [0] do array. A invariante desse sistema é todo nó deve ser melhor que suas crianças, ou, nesse caso, menor.
Não iremos armazenar ponteiros para as crianças, mas sim usar aritmética dos indexes, isso é um array afinal de contas.
O array de exemplo:
index: 0 1 2 3 4 5
valor: 10 20 30 25 40 35
Representa esta árvore:
10 <- root = melhor
/ \
20 30 <- crianças de 0
/ \ /
25 40 35 <- crianças de 1 e 2.
A árvore existe dentro dos índices do array.
A regra é: criança a esquerda do índice i = 2i + 1 criança a direita do índice i = 2i + 2 pai do índice i = (i-1) / 2
Parece complicado a princípio, mas depois de bater cabeça um pouco faz sentido e fica fácil.
Após qualquer operação a invariante será quebrada e deverá ser restaurada. Isso significa que, em caso de push, iremos inserir o novo nó no último index, no final do array. Provavelmente esse nó estará no lugar errado, por isso disse que a invariante foi quebrada.
Para resolver comparamos o novo nó com seu nó pai (que estará em (i-1) / 2), e se a criança, o novo nó, for melhor que seu nó pai iremos troca-los de lugar.
Nesse ponto é só questão de continuar repetindo esse processo até que novo nó seja pior que seu nó pai. Isso fará com que ele vá subindo a árvore até se encaixar onde deveria estar. Assim, restaurando a invariante da min-heap (todo nó deve ser melhor que suas crianças).
O mesmo acontece em caso de pop, mas nesse caso o nó root, index 0, é retirado e o último nó é colocado no lugar, quebrando a invariante. Daqui em diante é parecido com o push, mas nesse caso o novo root é que irá descer. Iremos comparar as crianças para achar a melhor e então comparar com o root. Se a melhor criança for melhor do que o root, trocam de lugar. E o processo de repete até que o nó seja encaixado no lugar correto, nesse caso quando todas crianças forem piores do que ele.
A heap não decide sozinha qual nó é melhor ou pior, ela delega essa comparação a uma função, que será o comparador.
A função fará o seguinte:
- Se a->priority < b->priority, então a é melhor (min-heap, menor = melhor)
- Se a->priority > b->priority, então b é melhor (min-heap, menor = melhor)
- Se a->priority == b->priority, então teremos que decidir por sequência, é melhor o nó que entrou primeiro na fila.
- Se a->priority == b->priority e os dois entraram ao mesmo tempo na fila, os nós são equivalentes. (fallback defensivo, nunca vai acontecer na simulação).
Na nossa implementação, podemos usar uma única função, que estará em t_heap, para realizar a lógica acima independente do scheduler ser FIFO ou EDF. Nos dois casos, a função segue a mesma.
O que muda é a priority de um node. A propriedade priority de cada node será definida com base no scheduler escolhido. Em caso de FIFO, node.priority = get_current_time(), já em caso de EDF será node.priority = coder->last_compile + sim->time_to_burnout.
É importante lembrar que t_heap_node, o node do exemplo, e t_coder, o coder do exemplo, são structs diferentes. Os nodes contém um ponteiro para o coder em questão, que quer receber o dongle, um long priority, que é a prioridade, e um long seq, que é o contador que incrementa a cada novo nó e usado para desempates.