Pular para o conteúdo

    Capítulo 29, Avançado

    Memória por dentro: strides, contiguidade e ordem

    Um array é um bloco de bytes mais uma descrição de como lê-lo. Quando você entende essa descrição, entende por que fatiar é de graça, por que transpor é de graça, e por que algumas operações são mais lentas do que deveriam.

    Código deste capítulo: avancado/cap29_memoria_strides.py

    O array é um bloco mais uma lista de `strides`

    Os dados ficam em um bloco contínuo de memória. O que dá forma a ele são os strides: quantos bytes pular para avançar uma posição em cada eixo. Um array de 3 por 4 de int64 (8 bytes por elemento) em ordem de linhas (ordem C, o padrão) avança 8 bytes para a próxima coluna e 32 (4 colunas × 8) para a próxima linha. A transposta não move um byte: só troca os strides:

    avancado/cap29_memoria_strides.pylinhas 10 a 16
    import numpy as np
    
    a = np.arange(12, dtype=np.int64).reshape(3, 4)
    print(a.strides, a.flags["C_CONTIGUOUS"], a.flags["F_CONTIGUOUS"])
    
    t = a.T
    print(t.strides, t.flags["C_CONTIGUOUS"], t.flags["F_CONTIGUOUS"], np.shares_memory(a, t))
    
    Saída
    (32, 8) True False
    (8, 32) False True True
    

    A transposta é contígua em ordem Fortran (F, a ordem de colunas usada em Fortran e em MATLAB): os elementos da mesma coluna estão lado a lado. Os dois arrays compartilham a memória, e é por isso que a.T custa quase nada, mesmo em uma matriz de gigabytes.

    Fatiar só muda os strides

    Uma fatia com passo é uma nova leitura do mesmo bloco, com outros strides. Nenhum dado se move:

    avancado/cap29_memoria_strides.pylinha 21
    print(a[:, ::2].strides, a[::2].strides, a[:, 0].strides)
    
    Saída
    (32, 16) (64, 8) (32,)
    

    Pegar uma linha em cada duas dobra o stride do eixo das linhas (de 32 para 64), e pegar uma coluna em cada duas dobra o das colunas (de 8 para 16). E a coluna 0 isolada tem um stride de 32 bytes: para percorrê-la, o NumPy salta pela memória, e esse salto tem um custo.

    Por que a contiguidade importa

    O processador lê a memória em linhas de cache (blocos de 64 bytes). Percorrer elementos vizinhos aproveita cada linha de cache inteira. Percorrer com saltos grandes desperdiça: a cada elemento, uma linha de cache nova. Compare somar a mesma quantidade de elementos, contíguos e espaçados:

    avancado/cap29_memoria_strides.pylinhas 26 a 34
    import timeit
    
    x = np.ones(16_000_000, dtype=np.float32)
    contiguo = x[:1_000_000]
    espacado = x[::16]
    print(contiguo.size == espacado.size)
    t_contiguo = min(timeit.repeat(lambda: contiguo.sum(), number=5, repeat=5))
    t_espacado = min(timeit.repeat(lambda: espacado.sum(), number=5, repeat=5))
    print("o contíguo foi mais rápido:", t_contiguo < t_espacado)
    
    Saída
    True
    o contíguo foi mais rápido: True
    

    São os mesmos 1 milhão de elementos, mas o espaçado precisa percorrer 16 vezes mais memória. Quando uma operação é inesperadamente lenta, os strides são o primeiro suspeito, e np.ascontiguousarray(x) faz uma cópia contígua quando vale a pena.

    FlagSignifica
    C_CONTIGUOUSLinhas contínuas na memória (padrão)
    F_CONTIGUOUSColunas contínuas na memória
    OWNDATAEste array é dono da sua memória (não é uma visão)
    WRITEABLEPode ser alterado (broadcast_to e janelas são somente leitura)

    Reinterpretar bytes com `view`

    O view com outro dtype não converte os valores: lê os mesmos bytes de outro jeito. É útil para inspecionar a representação interna de números e para processar dados binários:

    avancado/cap29_memoria_strides.pylinhas 39 a 43
    inteiros = np.array([1, 256, 65536], dtype=np.int32)
    print(inteiros.view(np.uint8))
    
    bits = np.array([1.0], dtype=np.float32).view(np.uint32)[0]
    print(hex(int(bits)))
    
    Saída
    [1 0 0 0 0 1 0 0 0 0 1 0]
    0x3f800000
    

    O resultado do primeiro view mostra que o NumPy guarda os inteiros em ordem little-endian nesta máquina (o byte menos significativo primeiro). O 0x3f800000 é a representação binária do número 1,0 em float32, segundo o padrão IEEE 754.

    `as_strided`: poder com perigo

    A função as_strided constrói qualquer visão que você descrever com shape e strides. Com ela você monta janelas deslizantes à mão. É a base do que o sliding_window_view do capítulo 28 faz com segurança:

    avancado/cap29_memoria_strides.pylinhas 48 a 52
    from numpy.lib.stride_tricks import as_strided
    
    y = np.arange(6)
    janelas = as_strided(y, shape=(4, 3), strides=(y.strides[0], y.strides[0]))
    print(janelas)
    
    Saída
    [[0 1 2]
     [1 2 3]
     [2 3 4]
     [3 4 5]]
    

    Não use as_strided sem necessidade

    O as_strided não verifica nada. Uma forma ou um stride que ultrapasse o bloco de memória lê (ou escreve) bytes que não pertencem ao array, sem erro e sem aviso. Isso corrompe dados ou derruba o programa de forma imprevisível. Prefira sempre o sliding_window_view, que calcula tudo para você, e reserve o as_strided para quem tem uma razão forte e testa com muito cuidado.

    Exercício 1

    Descrever a memória

    Escreva passos_em_bytes(a) que devolva os strides de um array e e_visao(a) que diga se ele não é dono dos seus dados. Teste com np.zeros((2, 3), dtype=np.int16) e uma fatia dele.