4.19. Pools de memória

Uma câmara que mantém três fotogramas em resolução completa num pool de framebuffer, executa em paralelo um buffer de pré-visualização separado e ainda tem espaço para um script Python e os seus objetos está a gerir mais memória do que um único bloco de RAM no MCU conseguiria fornecer. O MicroPython encaixa tudo isto distribuindo-o pelos vários tipos distintos de memória que o MCU disponibiliza, e encaminhando cada tipo de alocação para o tipo de memória de que realmente necessita.

4.19.1. Tipos de memória

Um MCU moderno da OpenMV Cam expõe quatro tipos distintos de memória. O primeiro é invisível para a aplicação; os outros três são pools de onde as alocações podem provir.

  • A cache de dados do CPU – uma região pequena e muito rápida de memória que se situa entre o CPU e o restante da RAM. Quando o CPU lê ou escreve um valor da memória principal, a cache mantém automaticamente uma cópia, de modo que acessos repetidos aos mesmos dados permanecem na cache sem nunca pagar o custo de aceder à memória mais lenta. A cache não é um pool de onde provêm alocações. É transparente para a aplicação – simplesmente faz com que o resto da RAM pareça mais rápida na prática do que a sua latência bruta sugeriria, até ao ponto em que um conjunto de trabalho deixa de caber nela.

  • Memória de processador fortemente acoplada – um pequeno bloco de RAM ligado diretamente ao CPU sem qualquer barramento entre eles. Acesso em ciclo único, sem falhas de cache, sem esperas. As alocações que genuinamente necessitam da memória mais rápida possível – onde cada ciclo de latência é relevante – provêm deste pool.

  • Memória rápida no chip – alguns centenas de kilobytes até cerca de um megabyte de RAM, integrada no encapsulamento do MCU. Baixa latência, grande largura de banda, mas tamanho limitado. O heap do MicroPython reside aqui para que os acessos a objetos Python se mantenham rápidos; buffers de trabalho mais pequenos que o CPU acessa com frequência partilham este pool.

  • Memória a granel mais lenta – em placas que combinam o MCU com um chip de memória externo, dezenas de megabytes de RAM fora do chip acedidos através do barramento externo. Muito maior, mas cada acesso demora mais do que a memória no chip; a cache de dados do CPU oculta grande parte desse custo para conjuntos de trabalho que consegue armazenar, e a diferença manifesta-se em operações que percorrem dados demasiado grandes para a cache. Utilizada para alocações que têm de ser grandes e que o CPU consegue tolerar a uma velocidade mais baixa – mais importante, o pool de framebuffer.

As placas desta família distribuem-se num espectro: algumas têm apenas RAM no chip; outras combinam RAM no chip com um bloco externo muito maior. Cada um dos três tipos alocáveis é tratado como um pool de memória – um bloco de onde provêm as alocações – e rotulado para que cada pedido possa solicitar o tipo de memória de que realmente necessita.

4.19.2. O framebuffer primário

O framebuffer que suporta snapshot() não solicita memória rápida. Solicita memória suficiente – nada mais. Isso coloca-o no pool que for maior, pelo que numa placa com memória no chip e externa, o framebuffer fica no bloco externo.

Um framebuffer em resolução completa com triplo buffering é demasiado grande para caber no pool rápido no chip na maioria dos componentes; o pool maior é o único que consegue alojá-lo. A cache de dados do CPU oculta grande parte do custo por acesso quando a aplicação processa a imagem, e o motor DMA que preenche o framebuffer a partir do sensor acompanha a taxa de dados do sensor em qualquer dos casos.

O tamanho exato que o framebuffer ocupa é calculado a partir do pixformat(), framesize() e contagem de framebuffers() atuais; cresce ou diminui sempre que algum destes muda.

4.19.3. Framebuffers secundários do sensor

Uma segunda instância de CSI obtém o seu próprio framebuffer, alocado do mesmo pool que o primário utiliza. O pool é partilhado; os buffers são independentes. A pegada do sensor secundário é normalmente muito menor do que a do primário, porque os sensores secundários operam a resoluções mais baixas, pelo que a memória extra que o segundo framebuffer ocupa representa uma fração pequena da do primário.

4.19.4. O framebuffer de streaming

O buffer de pré-visualização de imagem é a exceção. Não é alocado em nenhum dos pools em tempo de execução; é uma região fixa reservada em tempo de compilação, com um endereço e tamanho conhecidos. Isso mantém o caminho de pré-visualização fora do alcance de todas as outras alocações – a região existe desde o arranque e nunca se move.

4.19.5. O heap do MicroPython

Os objetos Python – variáveis, listas, dicionários, instâncias de classes, o wrapper Image que uma chamada a snapshot() devolve, todas as strings e tuplos que a aplicação cria – residem no heap com recolha de lixo do MicroPython, que é separado dos pools de memória da câmara. O heap com recolha de lixo (GC) é uma região de memória que o MicroPython gere internamente: o código Python aloca a partir dele implicitamente sempre que um objeto é criado, e o MicroPython analisa periodicamente o heap e reclama o espaço ocupado por objetos que a aplicação já não referencia, pelo que a aplicação nunca tem de libertar nada manualmente.

Uma região dedicada é reservada para o heap GC no arranque, tipicamente colocada na memória rápida no chip para que os acessos Python se mantenham rápidos, com um transbordo opcional para o bloco externo maior nas placas que necessitam de mais espaço para estruturas de dados grandes.

O Image devolvido por snapshot() é um pequeno objeto wrapper no heap GC; os dados de pixel subjacentes residem no framebuffer num dos pools da câmara. Os dois nunca competem pela mesma memória.

4.19.6. A juntar tudo

Encaminhar cada tipo de alocação para o pool certo – buffers grandes para o pool maior onde cabem, dados sensíveis à latência para os pools mais rápidos, o heap Python para a sua própria região, a pré-visualização para o seu espaço reservado – é o que torna possível executar um pipeline de captura em resolução completa, um canal de pré-visualização e um script Python não trivial em simultâneo em componentes que têm apenas alguns megabytes de memória rápida no total.