σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
RoadmapsRoteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
σμimplied vol · KKmax(S−K,0)udβρᵢⱼfat tailsPnL · drawdownyield · TdSₜ = μSₜ dt + σSₜ dWₜC = S·N(d₁) − K·e⁻ʳᵀ·N(d₂)σₚ² = wᵀΣwd₁ = [ln(S/K) + (r + σ²⁄2)T] ⁄ σ√T∂V/∂t + ½σ²S²·∂²V/∂S² + rS·∂V/∂S − rV = 0Sharpe = (Rₚ − R_f) ⁄ σₚVaR₉₉ = μ − 2.33σρ = Cov(X,Y) ⁄ σₓσᵧE[Rₚ] = Σ wᵢ·E[Rᵢ]φ(x) = e^(−x²/2) ⁄ √2πE[Rᵢ] = R_f + βᵢ(E[Rₘ] − R_f)dXₜ = θ(μ − Xₜ)dt + σ dWₜσ²ₜ = ω + α·ε²ₜ₋₁ + β·σ²ₜ₋₁C − P = S − K·e⁻ʳᵀΔ = ∂V/∂S Γ = ∂²V/∂S²p = (e^(rΔt) − d) ⁄ (u − d)rₜ = ln(Pₜ ⁄ Pₜ₋₁)E[Xₜ₊₁ | ℱₜ] = Xₜf* = (bp − q) ⁄ bz = (x − μ) ⁄ σP(N=k) = λᵏe^(−λ) ⁄ k!Θ = −∂V/∂Tν = S·φ(d₁)·√Tmin wᵀΣw s.t. wᵀμ = μ*∫Σ∂∞λ
uantCraft AcademyRoadmapsRoteiros
Ctrl K
AO
+1,2 mil alunos
EntrarParticipar ↗
RoadmapsRoteiros
AO
+1,2 mil alunos
Entrar
  • English
  • Français
  • Español
  • العربية
  • Português
  • Deutsch
  • 日本語
  • 中文
Roteiro de Cache locality›Módulo 3 · A máquina como um todo›Tópico 3.7·~1 h 30 minAvançado

Projeto final: reorganizar uma tabela de ticks e provar a redução de faltas

Posição no roteiroMódulo 3 de 3 · Tópico 7 de 7
3.7 Projeto final: reorganizar uma tabela de ticks e provar a redução de faltas
—Nota do lab— / 100XP total—Sequência—

6 min de leitura3 exemplos de código

  1. 01Uma tabela, três tarefas
  2. 02Uma disposição é uma lista de grupos de colunas
  3. 03Contar, precificar e provar
  4. 04O que você vai construir

Tudo o que esta habilidade ensinou se junta numa tabela de ticks. Oito colunas, três tarefas que a leem de jeitos diferentes e três disposições na memória. Você modela o endereço de cada valor, conta as faltas de cada tarefa com o cache que construiu, as precifica com o modelo de latência e prova qual disposição vence. Nenhuma vence todas as tarefas; a híbrida vence a mistura, com 1,5 vezes menos faltas que o array de structs.

01

Uma tabela, três tarefas

Uma tabela de ticks tem 8 colunas de 8 bytes: um timestamp, os preços e tamanhos de bid e ask, o preço do negócio, seu tamanho e alguns flags. Uma linha tem exatamente 64 bytes, o tamanho de uma linha de cache. Três tarefas leem essa tabela. Uma varredura lê a coluna de preço de cada linha, para uma média móvel. Uma tarefa de cotação lê o bid, o ask e o tamanho do bid de cada linha, para montar um livro. Uma consulta lê o tick completo de 1000 linhas escolhidas pela tabela, como faz um depurador ou um replay. A memória do primeiro tópico tem uma hierarquia, a do segundo uma linha de 64 bytes e a do quinto duas disposições de uma tabela: o array de structs (os valores de uma linha juntos) e o struct de arrays (cada coluna separada). Agora há um terceiro caminho, o híbrido, que mantém juntas as colunas quentes de cotação e juntas as colunas frias.

02

Uma disposição é uma lista de grupos de colunas

As três disposições são uma só ideia. Uma disposição é uma lista de grupos de colunas. Os valores de um grupo são guardados linha após linha e os grupos se seguem. Um grupo com todas as colunas é o array de structs; um grupo por coluna é o struct de arrays; dois grupos de quatro colunas são o nosso híbrido. Assim uma função dá o endereço de qualquer valor em qualquer disposição: pular os grupos antes do que contém a coluna e avançar row * len(group) + position valores dentro dele.

main.pyPython
def address(groups, row, col, n_rows, elem_bytes=ELEM_BYTES):    base = 0    for group in groups:        if col in group:            return base + (row * len(group) + group.index(col)) * elem_bytes        base += n_rows * len(group) * elem_bytes1    raise ValueError("column %d is in no group" % col)print(address([[0, 1, 2]], 2, 1, 10))print(address([[0], [1], [2]], 2, 1, 10))print(address([[1, 2], [0]], 3, 0, 10))
56
96
184
  1. 1Um grupo ocupa n_rows * len(group) * elem_bytes bytes: o grupo seguinte começa logo depois.

Confira os três resultados à mão. Com um grupo de três colunas, a linha 2 coluna 1 é o valor número 2 × 3 + 1 = 7, no byte 56. Com um grupo por coluna, a coluna 1 começa depois dos 10 valores da coluna 0, no byte 80, e a linha 2 fica 16 bytes adiante: 96. No híbrido [[1, 2], [0]] o grupo da coluna 0 vem em segundo, depois de 10 linhas de 2 valores (160 bytes), e a linha 3 fica 24 bytes adiante: 184.

Um endereço é tudo de que o modelo precisa

O cache nunca vê colunas nem linhas, só endereços. Por isso poucas linhas bastam para comparar disposições que em C++ exigiriam structs e laços diferentes.

03

Contar, precificar e provar

Uma tarefa é a lista de colunas que ela lê em cada linha que visita. Seu trace é, para cada linha em ordem, os endereços dessas colunas; o cache transforma o trace em faltas; e o modelo de latência do primeiro tópico transforma faltas em tempo, com 1 ns por acerto e 80 ns por falta. A comparação é então um laço sobre as disposições e a prova é o menor número. Rode as três tarefas da aula em 4000 ticks e você vê que a melhor disposição depende da tarefa.

main.pyPython
def workload_trace(groups, n_rows, reads, rows):    return [address(groups, row, col, n_rows) for row in rows for col in reads]def layout_misses(groups, n_rows, reads, rows=None):    rows = range(n_rows) if rows is None else rows    return count_misses(workload_trace(groups, n_rows, reads, rows))def compare_layouts(layouts, n_rows, reads, rows=None):    return {name: layout_misses(groups, n_rows, reads, rows) for name, groups in layouts.items()}def best_layout(misses):    return min(misses, key=misses.get)def time_ns(accesses, misses, hit_ns=HIT_NS, miss_ns=MISS_NS):    return (accesses - misses) * hit_ns + misses * miss_nsprint(workload_trace([[0, 1]], 4, [1, 0], [2]))for job, reads in WORKLOADS.items():    rows = sampled_rows(4000) if job == "lookup" else None    result = compare_layouts(LAYOUTS, 4000, reads, rows)    print(job, result, "->", best_layout(result))
[40, 32]
scan {'aos': 4000, 'soa': 500, 'hybrid': 2000} -> soa
quote {'aos': 4000, 'soa': 1500, 'hybrid': 2000} -> soa
lookup {'aos': 1000, 'soa': 8000, 'hybrid': 2000} -> aos

A varredura quer uma só coluna: o struct de arrays empacota oito preços por linha (500 faltas) e o array de structs desperdiça sete oitavos de cada linha que carrega (4000 faltas). A consulta pontual quer os oito valores de uma linha: o array de structs usa uma linha (1000 faltas) e o struct de arrays oito (8000). A cotação lê três das quatro colunas do grupo quente, então o híbrido é bom mas não o melhor. Qual disposição escolher depende da frequência de cada tarefa, e é isso que a mistura mede.

Meça antes de reorganizar

Uma disposição que vence as varreduras pode perder as consultas por um fator de oito. Antes de mudar a estrutura de dados de um programa real, anote quais tarefas a leem e com que frequência, e conte as faltas de cada disposição como aqui.

main.pyPython
def study(bars):    n_rows = min(int(bars.shape[0]), 4000)    misses = {}    accesses = 0    for job, reads in WORKLOADS.items():        rows = sampled_rows(n_rows) if job == "lookup" else None        misses[job] = compare_layouts(LAYOUTS, n_rows, reads, rows)        accesses += len(reads) * (len(rows) if rows is not None else n_rows)    mixed = {name: sum(misses[job][name] for job in WORKLOADS) for name in LAYOUTS}    best = best_layout(mixed)    return {"mixed": mixed, "mixed_best": best, "reduction": mixed["aos"] / mixed[best],            "ns": {name: time_ns(accesses, mixed[name]) for name in LAYOUTS}}out = study(load_bars())print(out["mixed"], "->", out["mixed_best"])print(round(out["reduction"], 2), "times fewer misses than the array of structs")print({name: round(ns / 1000) for name, ns in out["ns"].items()}, "microseconds")
{'aos': 9000, 'soa': 10000, 'hybrid': 6000} -> hybrid
1.5 times fewer misses than the array of structs
{'aos': 735, 'soa': 814, 'hybrid': 498} microseconds

Somando, o array de structs falta 9000 vezes, o struct de arrays 10 000 e o híbrido 6000. O híbrido não vence nenhuma tarefa de forma clara e não perde nenhuma feio: as varreduras e as cotações custam o dobro do que paga o struct de arrays, as consultas o dobro do que paga o array de structs, e a mistura favorece a disposição que nunca é péssima. No modelo de latência são 498 µs contra 735 µs do array de structs: 1,5 vez menos faltas e um terço a menos de tempo. Essa é a prova que o projeto final pede, e vale só para esta mistura de tarefas.

DefiniçãoColunas quentes e frias

As colunas quentes são as que a maioria das tarefas lê junto (aqui bid, ask e os dois tamanhos); as frias são lidas raramente (timestamp, preço, tamanho, flags). Manter cada tipo junto faz as tarefas comuns tocarem poucas linhas e as raras continuarem baratas.

O que dizem os dados reais4000 ticks de 8 colunas, cache de 32 KB de 8 vias, acerto 1 ns, falta 80 ns
Varredura do preço, struct de arrays500faltas (array de structs: 4000)
Consulta de 1000 ticks, array de structs1,000faltas (struct de arrays: 8000)
A mistura, disposição híbrida6,000faltas (9000 e 10 000 as outras)
Redução em relação ao array de structs1.5×menos faltas (735 µs → 498 µs)
Faltas da mistura por disposição
Struct de arrays10000
Array de structs9000
Híbrida6000
04

O que você vai construir

O laboratório segue a aula: o endereço de um valor em qualquer disposição, o trace de uma tarefa, as faltas de uma tarefa em uma disposição e em todas, a melhor disposição, o tempo de uma tarefa e o estudo da tabela de ticks com a prova da mistura.

  • Função · perguntaBaseia-se em
  • address(groups, row, col, n_rows)Onde está este valor na memória?Aritmética simples
  • workload_trace(groups, n_rows, reads, rows)Quais endereços a tarefa toca?address
  • layout_misses(groups, n_rows, reads, rows)Quantas faltas nesta disposição?workload_tracecount_misses
  • compare_layouts(layouts, n_rows, reads, rows)E em cada disposição?layout_misses
  • best_layout(misses)Qual vence?Aritmética simples
  • time_ns(accesses, misses)Quanto custa em tempo?Aritmética simples
  • study(bars)Qual disposição vence a mistura de tarefas?compare_layoutsbest_layouttime_ns

Pontos-chave

  • Uma disposição é uma lista de grupos de colunas. Um grupo é o array de structs, um por coluna é o struct de arrays, e o meio-termo é um híbrido; uma só função dá o endereço em todas.
  • A tarefa decide a disposição. O struct de arrays venceu as varreduras com 500 faltas e perdeu as consultas com 8000; o array de structs fez o oposto.
  • Prove com uma contagem. Conte as faltas da mistura real de tarefas, precifique com o modelo de latência e escolha a disposição com menos: aqui a híbrida, com 6000 faltas contra 9000.
Verificação rápida
Uma tarefa lê só a coluna de preço de 4000 linhas. No struct de arrays, cada valor ocupa 8 bytes e uma linha guarda 64. Quantas linhas ela toca?
Escolha uma resposta.

Próxima habilidade: o lado de memória do desempenho está feito. As próximas habilidades da trilha de otimização passam para o processador: executar a mesma operação em muitos valores de uma vez em registradores vetoriais e compartilhar dados entre núcleos sem travas.

Lab: o que é avaliado

7 tarefas · 100 pts
  • O endereço de um valor20 pts
  • O trace de uma tarefa15 pts
  • As faltas de uma tarefa10 pts
  • Cada disposição10 pts
  • A melhor disposição5 pts
  • O tempo de uma tarefa10 pts
  • O estudo com a tabela do SPY30 pts
Explorador · lab-cl-13
▾ lab-cl-13
▾ data
…
bash · no navegador
learner@quantcraft:~/lab-cl-13$
Python ainda não carregadoPythonLn 1, Col 1Spaces: 4UTF-8

Redefinir este tópico?

Tudo em lab-cl-13 volta aos arquivos iniciais e o terminal é limpo. Sua solução atual será perdida — isso não pode ser desfeito.

Será redefinido
  • main.py → código inicial
  • Histórico do terminal e saída dos testes
  • Alterações não enviadas
Será mantido
  • Melhor nota do lab
  • Seu XP e sua sequência
  • Progresso da aula e anotações