Reduzindo mais de 90% da latência na Rinha de Backend 2026 executando FAISS diretamente dentro das APIs .NET

Michel Oliveira
6 min read
Reduzindo mais de 90% da latência na Rinha de Backend 2026 executando FAISS diretamente dentro das APIs .NET

Introdução

Na primeira versão da minha solução para a Rinha de Backend 2026, a busca vetorial rodava em um serviço separado em Python utilizando FAISS (Facebook AI Similarity Search), enquanto as APIs HTTP eram implementadas em .NET Native AOT.

A arquitetura funcionava bem e conseguia operar dentro das constraints da competição a ~100ms de latência p99 em:

  • 1 vCPU.
  • 350 MB de RAM.
  • 3 milhões de vetores.

O índice IVF (Inverted File Index) com quantização FP16 (Floating Point 16-bit) já resolve o principal problema matemático da Rinha: Tornar viável a busca aproximada em milhões de vetores sem brute force.

Só que, depois disso, o gargalo deixou de estar na busca vetorial. O custo dominante passou a ser a movimentação de dados entre processos e stacks.

Repositórios

Por que o Python virou gargalo

A escolha do Python aconteceu principalmente por um motivo bastante simples: O FAISS possui bindings oficiais e maduros para Python, enquanto o ecossistema .NET não possui.

Atualmente, a alternativa mais estável e documentada continua sendo utilizar diretamente os bindings oficiais mantidos pela Meta / Facebook (ou chamar diretamente em C++).

A segunda versão da solução passa a utilizar a biblioteca Binozo/Faiss.NET, ainda em versão preview, permitindo integrar o FAISS diretamente dentro do processo das APIs .NET através de interop com a biblioteca nativa em C++.

A primeira arquitetura

A arquitetura inicial ficou organizada desta forma:

flowchart LR lb[HAProxy] subgraph apis["APIs .NET Native AOT"] api1[API 1] api2[API 2] end py[Python + FAISS] lb --> api1 lb --> api2 api1 --> py api2 --> py

Cada requisição seguia basicamente este fluxo:

  1. A API recebia o JSON.
  2. Gerava o vetor.
  3. Serializava os dados.
  4. Enviava via HTTP para o processo Python.
  5. O Python executava a busca FAISS.
  6. A resposta era serializada novamente.
  7. O resultado voltava para a API.

O problema é que o overhead arquitetural pesa muito.

Mesmo rodando local, ainda existem:

  • Serialização e desserialização.
  • Cópia de buffers.
  • Syscall de socket.
  • Context switching.
  • Runtime Python.
  • Servidor HTTP interno.

O FAISS já executava rápido. O problema estava em tudo que acontecia ao redor dele.

O que levou à mudança

Em determinado momento ficou claro que o serviço Python praticamente não possuía lógica própria.

Ele funcionava basicamente como uma ponte entre a API .NET e a biblioteca nativa do FAISS, sendo que os bindings Python apenas encapsulam chamadas para o código C++.

O fluxo ficava algo próximo disso:

sequenceDiagram participant NET as .NET API participant HTTP as HTTP participant PY as Python participant B as Python Bindings participant F as FAISS C++ NET->>HTTP: Serializa vetor HTTP->>PY: Envia request PY->>B: Chama bindings B->>F: Executa busca vetorial F-->>B: Resultado B-->>PY: Retorna vizinhos PY-->>HTTP: Serializa resposta HTTP-->>NET: Retorna resultado

A partir disso, a pergunta passou a ser inevitável:

por que não chamar a biblioteca C++ diretamente?

A nova arquitetura

A segunda versão remove completamente o processo Python. O FAISS passa a ser carregado diretamente dentro das APIs .NET via interop.

flowchart LR lb[HAProxy] subgraph apis["APIs .NET Native AOT"] api1[API 1 + FAISS] api2[API 2 + FAISS] end lb --> api1 lb --> api2

A diferença é enorme. A latência cai de:

112.40ms

para:

10.47ms

Mais de 90% de redução sem mudanças relevantes no algoritmo vetorial.

O IVF continua o mesmo. O FP16 continua o mesmo. A principal mudança acontece na arquitetura.

O que deixa de existir

Quando o índice roda dentro do mesmo processo da API, o caminho crítico da requisição fica muito menor.

Antes:

sequenceDiagram participant C as Cliente participant API as API .NET participant PY as Python + FAISS C->>API: HTTP Request API->>PY: HTTP interno + JSON PY->>PY: Busca vetorial PY-->>API: JSON resposta API-->>C: Resultado

Depois:

sequenceDiagram participant C as Cliente participant API as API .NET + FAISS C->>API: HTTP Request API->>API: Busca vetorial local API-->>C: Resultado

Isso elimina:

  • HTTP interno.
  • Serialização JSON intermediária.
  • Desserialização.
  • Buffers TCP.
  • Cópia excessiva de memória.
  • Troca de contexto entre processos.
  • Runtime Python.
  • Serviço auxiliar.

O vetor sai praticamente direto do parser HTTP para a biblioteca nativa.

Trade-offs

A nova arquitetura fica muito mais eficiente, mas também mais instável.

Os bindings Python escondem praticamente toda a complexidade do FAISS.

Quando o acesso passa a ocorrer diretamente via interop, começam a aparecer problemas como:

  • ABI (Application binary interface) compatibility.
  • Gerenciamento manual de memória.
  • Alinhamento de estruturas.
  • Lifecycle manual do índice.
  • Debugging nativo.
  • Ponteiros inválidos.
  • Crashes fora do runtime gerenciado.

Parte da simplicidade operacional é trocada por eficiência.

Resultados oficiais

Results

A versão final evidencia o impacto da integração direta do FAISS nas APIs .NET Native AOT.

  • p99: 10.47ms.
  • Pontos: 4.610,99.

Em relação à arquitetura anterior (~112ms), a redução representa mais de 90% da latência e o incremento de 1.030,9 pontos.

Resultados locais

Local Test

No teste local, observa-se p99 de 0.75ms. A diferença decorre do ambiente de execução, já que o hardware utilizado no benchmark local é um Mac Mini, conforme descrito no repositório da Rinha.

Execução do benchmark no GitHub Actions: GitHub Actions Run (Rinha de Backend 2026 - FAISS .NET AOT)

Próximos passos

Os próximos ajustes focam em duas frentes principais: Redução adicional de latência e melhoria de acurácia, mitigando os falso-positivos que impactam diretamente a pontuação no benchmark.

Nos próximos dias, estarei fazendo otimizações no pipeline de busca e no uso do índice vetorial, além de ajustes finos nos parâmetros para equilibrar desempenho e precisão.

A evolução dessa versão será compartilhada conforme os experimentos forem sendo validados.

Conclusão

A primeira versão resolve corretamente o problema matemático da Rinha 2026 usando IVF e quantização FP16. A segunda resolve o problema arquitetural.

Ao remover completamente o processo Python e integrar o FAISS diretamente dentro das APIs .NET Native AOT, praticamente todo o overhead intermediário desaparece.

O resultado é uma redução de latência de 112.40ms para 10.47ms sem mudanças substanciais no algoritmo de busca vetorial.

Depois que a busca fica suficientemente eficiente, o maior custo do sistema deixa de ser computação e passa a ser movimentação de dados entre camadas desnecessárias.

Referências

Conecte-se para transformar sua tecnologia!

Saiba mais e entre em contato: