Nei titoli e nei testi troverete qualche rimando cinematografico (ebbene si, sono un cinefilo). Se non vi interessano fate finta di non vederli, già che non sono fondamentali per la comprensione dei post...

Di questo blog ho mandato avanti, fino a Settembre 2018, anche una versione in Spagnolo. Potete trovarla su El arte de la programación en C. Buona lettura.

domenica 20 gennaio 2019

Io e l'Integer Packing
come funziona il pack e unpack di interi in C

Annie: Oh, vai dallo psichiatra?
Alvy: S-sì. Da quindici anni soli...
Annie: Quindici anni?!
Alvy: Sì... gli concedo un altro anno... poi vado a Lourdes.
Nel capolavoro Io e Annie il Maestro Woody Allen ci proponeva, attraverso una personalissima "commedia romantica nevrotica", l'analisi della relazione tra i protagonisti Alvy Singer e Annie Hall in tutte le sue fasi (l'inizio/la relazione/la fine), mettendo in mostra fatti e misfatti in cui tutti ci possiamo riconoscere. Allo stesso modo (ma non con la classe del Maestro, purtroppo) cercherò con questo articolo di ricreare e (spero) risolvere una situazione che tutti i programmatori che si sporcano le mani con la programmazione low-level hanno dovuto affrontare (...ebbene si, cari programmatori high-level: sotto le vostre splendide applicazioni gira sempre del Software low-level, quasi sempre scritto in C, non dimenticatevelo...).
...Alvy: il pack/unpack di interi... Annie: argomento interessantissimo (yawn)...
Supponiamo (ma proprio supponiamo) che abbiamo due interi da 16 bit che vogliamo, magicamente, trasformare in un unico intero da 32 bit. Oppure vogliamo fare l'operazione inversa (spezzare un int a 32 bit in due int a 16 bit). Oppure abbiamo quattro interi da 16 bit e vogliamo... blah, blah, bla. I casi appena descritti non sono fantascienza, anzi sono casi abbastanza normali: ad esempio chi maneggia giornalmente i registri di periferiche Modbus sicuramente si è trovato a dover fare operazioni di questo tipo. Ecco, chi usa queste operazioni frequentemente può anche saltare il resto dell'articolo (e magari usare il tempo risparmiato per guardarsi un buon film e alimentare così la propria mente).

Ma, visto che mi è capitato di vedere facce terrorizzate di fronte a problemoni come questo (specialmente tra i colleghi non avvezzi alla programmazione low-level), cercherò di proporre alcune soluzioni, dopodiché si noterà che il tutto è di una semplicità disarmante. Si può, ovviamente, risolvere in 33 (o 33000) modi diversi, io ho pensato di mostrare i 3 modi che mi sembrano più semplici, lineari ed efficienti. Gli esempi che seguono sono di pack e unpack di interi a 32 bit (2 registri da 16bit in un intero da 32 bit e viceversa), ma adattare gli esempi ai 64 bit (4 registri da 16bit in un intero da 64 bit e viceversa) è veramente un gioco da ragazzi, e quindi non ve lo mostro nemmeno. Vai col codice dell'esempio 1!:

1) pack32 e unpack32 con bitwise

// pack32bitwise - copia un array di 2 uint16_t dentro una variabile uint32_t
uint32_t pack32bitwise(
    const uint16_t *src)    // array sorgente per il pack
{
    // ottiene il valore destinazione dal valore sorgente
    return ((uint32_t)src[1]) << 16 | src[0];
}

// unpack32bitwise - copia una variabile uint32_t in un array di 2 uint16_t
uint16_t *unpack32bitwise(
    uint16_t       *dest,   // array destinazione
    const uint32_t src)     // valore sorgente per l'unpack
{
    // ottiene il valore destinazione dal valore sorgente
    dest[1] = (uint16_t)((src & 0xFFFF0000) >> 16);
    dest[0] = (uint16_t)( src & 0x0000FFFF);

    return dest;
}
Questo è il modo più semplice e (probabilmente) più efficiente per fare il pack/unpack (io di solito uso questo). Si effettuano delle semplici operazioni di bitwise, che spostano o copiano i valori di entrata sull'uscita. Ovviamente la funzione pack() si aspetta un array come argomento e restituisce il valore intero corrispondente. Specularmente la funzione unpack() si aspetta un argomento con un valore e un argomento destinazione che sarà un array. Le operazioni di bitwise sono semplicissime: usando lo shift a 16 (visto che usiamo interi a 16 bit) possiamo copiare/spostare/mascherare opportunamente per ottenere il risultato voluto. Ed ora siamo pronti per l'esempio 2. vai col codice!:

2) pack e unpack con union

// pack32union - copia un array di 2 uint16_t dentro una variabile uint32_t
uint32_t pack32union(
    const uint16_t *src)    // array sorgente per il pack
{
    union {
        uint16_t src[2];
        uint32_t dest;
    } u;

    // ottiene il valore destinazione dal valore sorgente
    u.src[0] = src[0];
    u.src[1] = src[1];

    return u.dest;
}

// unpack32union - copia una variabile uint32_t in un array di 2 uint16_t
uint16_t *unpack32union(
    uint16_t       *dest,   // array destinazione
    const uint32_t src)     // valore sorgente per l'unpack
{
    union {
        uint16_t dest[2];
        uint32_t src;
    } u;

    // ottiene il valore destinazione dal valore sorgente
    u.src = src;
    dest[0] = u.dest[0];
    dest[1] = u.dest[1];

    return dest;
}
Questa maniera per eseguire il pack/unpack è quasi un trucco: si sfrutta una particolarità del linguaggio (il funzionamento delle union) in maniera impropria (non è che le union servono esattamente a quello)... ma visto che funziona perché non farlo? Quindi basta creare una union della forma opportuna e, assegnato a un membro della union il valore in ingresso, troveremo nell'altro membro della union il valore "rimappato" di uscita, esattamente quello che ci serve, no? Per chi in questo momento ha un vuoto di memoria su come funzionano le union basta ricordarsi di questo: la dimensione di una union è quella di un membro solo (quello che occupa più memoria) e, nel nostro caso, visto che i due membri occupano lo stesso spazio ma sono di tipo diverso ci viene offerto un immediato recasting implicito. Semplicissimo! Ed ora passiamo all'ultimo esempio. vai col codice!:

3) pack e unpack con memcpy()

// pack32memcpy - copia un array di 2 uint16_t dentro una variabile uint32_t
uint32_t pack32memcpy(
    const uint16_t *src)    // array sorgente per il pack
{
    // copia il valore sorgente nella destinazione
    uint32_t dest;
    memcpy(&dest, src, sizeof(dest));

    return dest;
}

// unpack32memcpy - copia una variabile uint32_t in un array di 2 uint16_t
uint16_t *unpack32memcpy(
    uint16_t       *dest,   // array destinazione
    const uint32_t src)     // valore sorgente per l'unpack
{
    // copia il valore sorgente nella destinazione
    uint32_t mysrc = src;
    memcpy(dest, &mysrc, sizeof(mysrc));

    return dest;
}
Visto che le operazioni di pack/unpack sono, bene o male, operazioni di copia/spostamento di memoria, non poteva mancare un esempio con la memcpy(), che è l'oggetto privilegiato e preferito quando si parla di operazioni di questo tipo. Mi risulta che questo metodo è molto usato, e l'ho usato anch'io ma, ultimamente, uso più spesso il metodo 1 (anche perché col metodo 1 è facilissimo, nel caso risulti necessario, invertire l'ordine dei registri. Non è usuale ma a volte capita). Il codice è così semplice che non c'è quasi nulla da spiegare: sfruttando il fatto che la memcpy() usa argomenti di tipo void* è perfettamente usabile per una operazione come la nostra, che consiste, a tutti gli effetti, nella copia di un blocco di memoria di un tipo in uno di un tipo diverso. A questo punto cosa ci manca? ma un bell'esempio d'uso, chiaro! Eccolo:
#include <stdio.h>
#include <stdint.h>
#include <string.h>

// prototipi locali
uint32_t pack32bitwise(const uint16_t *src);
uint16_t *unpack32bitwise(uint16_t *dest, const uint32_t src);
uint32_t pack32union(const uint16_t *src);
uint16_t *unpack32union(uint16_t *dest, const uint32_t src);
uint32_t pack32memcpy(const uint16_t *src);
uint16_t *unpack32memcpy(uint16_t *dest, const uint32_t src);

// funzione main
int main(int argc, char *argv[])
{
    // set variabili per test pack32()
    uint16_t src1[2];
    uint32_t dest1;
    src1[0] = 10;
    src1[1] = 20;

    // test pack32()
    dest1 = pack32bitwise(src1);;
    printf("con bitwise: src1={%d,%d} dest1=%d\n", src1[0], src1[1], dest1);
    dest1 = pack32union(src1);;
    printf("con union:   src1={%d,%d} dest1=%d\n", src1[0], src1[1], dest1);
    dest1 = pack32memcpy(src1);;
    printf("con memcpy:  src1={%d,%d} dest1=%d\n", src1[0], src1[1], dest1);

    // set variabili per test unpack32()
    uint32_t src2 = 1310730;
    uint16_t dest2[2];

    // test unpack32()
    unpack32bitwise(dest2, src2);
    printf("con bitwise; src=%d dest={%d,%d}\n", src2, dest2[0], dest2[1]);
    unpack32union(dest2, src2);
    printf("con union:   src=%d dest={%d,%d}\n", src2, dest2[0], dest2[1]);
    unpack32memcpy(dest2, src2);
    printf("con memcpy:  src=%d dest={%d,%d}\n", src2, dest2[0], dest2[1]);

    return 0;
}
Ecco, con questo piccolo main() di esempio potrete verificare che le varie versioni di pack e unpack offrono sempre gli stessi risultati (e ci mancherebbe solo che non fosse così!). E con questo ho detto tutto, e vi lascio, come compiti delle vacanze (vacanze? quali vacanze?) la trasformazione di tutti gli esempi a 32 bit in esempi a 64 bit: come ho già detto più sopra, è un gioco da ragazzi!

Ciao e al prossimo post!

Nessun commento:

Posta un commento