web-dev-qa-db-fra.com

Si un entier 32 bits déborde, peut-on utiliser une structure 40 bits au lieu d'une structure longue 64 bits?

Si, par exemple, un entier 32 bits déborde, au lieu de mettre à niveau int vers long, pouvons-nous utiliser un type 40 bits si nous avons besoin d'une plage uniquement dans les 240, de sorte que nous économisons 24 (64-40) bits pour chaque entier?

Si c'est le cas, comment?

Je dois gérer des milliards et l'espace est une contrainte plus importante.

75
user1660982

Oui mais...

C'est certainement possible, mais c'est généralement absurde (pour tout programme qui n'utilise pas milliards de ces nombres):

#include <stdint.h> // don't want to rely on something like long long
struct bad_idea
{
    uint64_t var : 40;
};

Ici, var aura en effet une largeur de 40 bits au détriment de beaucoup un code moins efficace généré (il s'avère que "beaucoup" est tout à fait faux - la surcharge mesurée n'est que de 1 à 2%, voir les horaires ci-dessous), et généralement en vain. À moins que vous n'ayez besoin d'une autre valeur 24 bits (ou d'une valeur 8 et 16 bits) que vous souhaitez intégrer dans la même structure, l'alignement perdra tout ce que vous pourriez gagner.

Dans tous les cas, à moins que vous n'en ayez des milliards, la différence effective de consommation de mémoire ne sera pas perceptible (mais le code supplémentaire nécessaire pour gérer le champ de bits sera perceptible!).

Remarque:
La question a entre-temps été mise à jour pour refléter qu'en effet milliards de nombres sont nécessaires, donc cela peut être une chose viable à faire, en supposant que vous preniez des mesures pour ne pas perdre les gains dus à l'alignement et au remplissage de la structure, c'est-à-dire soit en stockant quelque chose d'autre dans les 24 bits restants, soit en stockant vos valeurs de 40 bits dans des structures de 8 chacune ou des multiples de celles-ci).
La sauvegarde de trois octets un milliard de fois est utile car elle nécessitera sensiblement moins de pages mémoire et entraînera ainsi moins de cache et de manquements TLB, et surtout des défauts de page (une seule pondération de défaut de page) des dizaines de millions d'instructions).

Bien que l'extrait ci-dessus n'utilise pas les 24 bits restants (il démontre simplement la partie "utiliser 40 bits"), quelque chose semblable au suivant sera nécessaire pour vraiment rendre l'approche utile dans le sens de préserver la mémoire - supposé que vous avez en effet d'autres données "utiles" à mettre dans les trous:

struct using_gaps
{
    uint64_t var           : 40;
    uint64_t useful_uint16 : 16;
    uint64_t char_or_bool  : 8;  
};

La taille et l'alignement de la structure seront égaux à un entier 64 bits, donc rien n'est perdu si vous faites par exemple un tableau d'un milliard de ces structures (même sans utiliser d'extensions spécifiques au compilateur). Si vous n'avez pas besoin d'une valeur 8 bits, vous pouvez également utiliser une valeur 48 bits et 16 bits (ce qui donne une plus grande marge de dépassement).
Alternativement, vous pouvez, au détriment de la convivialité, mettre 8 valeurs de 40 bits dans une structure (le plus petit multiple commun de 40 et 64 étant 320 = 8 * 40). Bien sûr, votre code qui accède aux éléments du tableau de structures deviendra beaucoup plus compliqué (bien que l'on puisse probablement implémenter un operator[] qui restaure la fonctionnalité du tableau linéaire et masque la complexité de la structure).

Mise à jour:
A écrit une suite de tests rapides, juste pour voir quelle surcharge les champs de bits (et la surcharge d'opérateur avec les références de champs de bits) auraient. Code publié (en raison de la longueur) à gcc.godbolt.org , la sortie de test de ma machine Win7-64 est:

Running test for array size = 1048576
what       alloc   seq(w)  seq(r)  Rand(w)  Rand(r)  free
-----------------------------------------------------------
uint32_t    0      2       1       35       35       1
uint64_t    0      3       3       35       35       1
bad40_t     0      5       3       35       35       1
packed40_t  0      7       4       48       49       1


Running test for array size = 16777216
what        alloc  seq(w)  seq(r)  Rand(w)  Rand(r)  free
-----------------------------------------------------------
uint32_t    0      38      14      560      555      8
uint64_t    0      81      22      565      554      17
bad40_t     0      85      25      565      561      16
packed40_t  0      151     75      765      774      16


Running test for array size = 134217728
what        alloc  seq(w)  seq(r)  Rand(w)  Rand(r)  free
-----------------------------------------------------------
uint32_t    0      312     100     4480     4441     65
uint64_t    0      648     172     4482     4490     130
bad40_t     0      682     193     4573     4492     130
packed40_t  0      1164    552     6181     6176     130

Ce que l'on peut voir, c'est que la surcharge supplémentaire des champs de bits est négligeable, mais la surcharge de l'opérateur avec une référence de champ de bits comme une commodité est plutôt drastique (augmentation d'environ 3 fois) lors de l'accès linéaire aux données d'une manière compatible avec le cache. D'un autre côté, sur l'accès aléatoire, cela n'a même guère d'importance.

Ces timings suggèrent qu'il serait préférable d'utiliser simplement des entiers 64 bits car ils sont globalement toujours plus rapides que les champs binaires (bien qu'ils touchent plus de mémoire), mais bien sûr, ils ne prennent pas en compte le coût des défauts de page avec des ensembles de données beaucoup plus grands. Cela peut sembler très différent une fois que vous n'avez plus de physique RAM (je n'ai pas testé cela).

82
Damon

Vous pouvez très efficacement compresser des entiers 4 * 40bits dans une structure 160 bits comme celle-ci:

struct Val4 {
    char hi[4];
    unsigned int low[4];
}

long getLong( const Val4 &pack, int ix ) {
  int hi= pack.hi[ix];   // preserve sign into 32 bit
  return long( (((unsigned long)hi) << 32) + (unsigned long)pack.low[i]);
}

void setLong( Val4 &pack, int ix, long val ) {
  pack.low[ix]= (unsigned)val;
  pack.hi[ix]= (char)(val>>32);
}

Ceux-ci peuvent à nouveau être utilisés comme ceci:

Val4[SIZE] vals;

long getLong( int ix ) {
  return getLong( vals[ix>>2], ix&0x3 )
}

void setLong( int ix, long val ) {
  setLong( vals[ix>>2], ix&0x3, val )
}
53
runec

Vous voudrez peut-être envisager le codage à longueur variable (VLE)

Vraisemblablement, vous avez stocké un grand nombre de ces numéros quelque part (dans la RAM, sur le disque, les envoyer sur le réseau, etc.), puis les prenez un par un et effectuez un traitement.

Une approche serait de les encoder en utilisant VLE. Depuis le protobuf de Google documentation (licence CreativeCommons)

Les varints sont une méthode de sérialisation des entiers en utilisant un ou plusieurs octets. Les petits nombres prennent un plus petit nombre d'octets.

Chaque octet dans un varint, à l'exception du dernier octet, a le bit le plus significatif (msb) défini - cela indique qu'il y a d'autres octets à venir. Les 7 bits inférieurs de chaque octet sont utilisés pour stocker la représentation du complément à deux du nombre dans des groupes de 7 bits, le groupe le moins significatif en premier.

Donc, par exemple, voici le numéro 1 - c'est un seul octet, donc le msb n'est pas défini:

0000 0001

Et voici 300 - c'est un peu plus compliqué:

1010 1100 0000 0010

Comment déterminez-vous qu'il s'agit de 300? Tout d'abord, vous supprimez le msb de chaque octet, car il est juste là pour nous dire si nous avons atteint la fin du nombre (comme vous pouvez le voir, il est défini dans le premier octet car il y a plus d'un octet dans le varint)

Avantages

  • Si vous avez beaucoup de petits nombres, vous utiliserez probablement moins de 40 octets par entier, en moyenne. Peut-être beaucoup moins.
  • Vous pouvez stocker de plus grands nombres (avec plus de 40 bits) à l'avenir, sans avoir à payer de pénalité pour les petits

Les inconvénients

  • Vous payez un bit supplémentaire pour chacun des 7 bits significatifs de vos numéros. Cela signifie qu'un nombre de 40 bits significatifs aura besoin de 6 octets. Si la plupart de vos nombres ont 40 bits significatifs, vous préférez une approche par champ de bits.
  • Vous perdrez la possibilité de passer facilement à un nombre en raison de son index (vous devez analyser au moins partiellement tous les éléments précédents d'un tableau afin d'accéder à l'élément actuel.
  • Vous aurez besoin d'une certaine forme de décodage avant de faire quoi que ce soit d'utile avec les nombres (bien que cela soit également vrai pour d'autres approches, comme les champs de bits)
25
Sam

(Edit: Tout d'abord - ce que vous voulez est possible et logique dans certains cas; j'ai dû faire des choses similaires lorsque j'ai essayé de faire quelque chose pour le défi Netflix et n'avais que 1 Go de mémoire; Deuxièmement - il est probablement préférable d'utiliser un tableau de caractères pour le stockage 40 bits pour éviter tout problème d'alignement et la nécessité de jouer avec les pragmas de struct struct; Troisièmement - cette conception suppose que vous êtes OK avec l'arithmétique 64 bits pour les résultats intermédiaires, ce n'est que pour les grands stockage de baie que vous utiliseriez Int40; Quatrièmement: je ne reçois pas toutes les suggestions que c'est une mauvaise idée, lisez simplement ce que les gens traversent pour emballer les structures de données maillées et cela ressemble à un jeu d'enfant en comparaison).

Ce que vous voulez, c'est une structure qui n'est utilisée que pour stocker des données sous forme d'entiers 40 bits, mais qui est implicitement convertie en int64_t pour l'arithmétique. La seule astuce consiste à faire l'extension de signe de 40 à 64 bits à droite. Si vous êtes d'accord avec des entrées non signées, le code peut être encore plus simple. Cela devrait pouvoir vous aider à démarrer.

#include <cstdint>
#include <iostream>

// Only intended for storage, automatically promotes to 64-bit for evaluation
struct Int40
{
     Int40(int64_t x) { set(static_cast<uint64_t>(x)); } // implicit constructor
     operator int64_t() const { return get(); } // implicit conversion to 64-bit
private:
     void set(uint64_t x)
     {
          setb<0>(x); setb<1>(x); setb<2>(x); setb<3>(x); setb<4>(x);
     };
     int64_t get() const
     {
          return static_cast<int64_t>(getb<0>() | getb<1>() | getb<2>() | getb<3>() | getb<4>() | signx());
     };
     uint64_t signx() const
     {
          return (data[4] >> 7) * (uint64_t(((1 << 25) - 1)) << 39);
     };
     template <int idx> uint64_t getb() const
     {
          return static_cast<uint64_t>(data[idx]) << (8 * idx);
     }
     template <int idx> void setb(uint64_t x)
     {
          data[idx] = (x >> (8 * idx)) & 0xFF;
     }

     unsigned char data[5];
};

int main()
{
     Int40 a = -1;
     Int40 b = -2;
     Int40 c = 1 << 16;
     std::cout << "sizeof(Int40) = " << sizeof(Int40) << std::endl;
     std::cout << a << "+" << b << "=" << (a+b) << std::endl;
     std::cout << c << "*" << c << "=" << (c*c) << std::endl;
}

Voici le lien pour l'essayer en direct: http://rextester.com/QWKQU25252

21
Stefan Atev

Vous pouvez utiliser une structure de champ binaire, mais cela ne vous fera pas économiser de mémoire:

struct my_struct
{
    unsigned long long a : 40;
    unsigned long long b : 24;
};

Vous pouvez compresser n'importe quel multiple de 8 de ces variables 40 bits dans une structure:

struct bits_16_16_8
{
    unsigned short x : 16;
    unsigned short y : 16;
    unsigned short z :  8;
};

struct bits_8_16_16
{
    unsigned short x :  8;
    unsigned short y : 16;
    unsigned short z : 16;
};

struct my_struct
{
    struct bits_16_16_8 a1;
    struct bits_8_16_16 a2;
    struct bits_16_16_8 a3;
    struct bits_8_16_16 a4;
    struct bits_16_16_8 a5;
    struct bits_8_16_16 a6;
    struct bits_16_16_8 a7;
    struct bits_8_16_16 a8;
};

Cela vous fera économiser de la mémoire (par rapport à l'utilisation de 8 variables 64 bits "standard"), mais vous devrez diviser chaque opération (et en particulier celles arithmétiques) sur chacune de ces variables en plusieurs opérations.

Ainsi, l'optimisation de la mémoire sera "échangée" contre des performances d'exécution.

16
barak manos

Comme le suggèrent les commentaires, c'est une tâche assez ardue.

Probablement un tracas inutile à moins que vous voulez enregistrer beaucoup de RAM - alors cela a beaucoup plus de sens. (L'économie de RAM serait la somme totale des bits enregistrés sur des millions de long valeurs stockées dans la RAM)

J'envisagerais d'utiliser un tableau de 5 octets/char (5 * 8 bits = 40 bits). Ensuite, vous devrez déplacer des bits de votre (int débordé - donc une valeur long) dans le tableau d'octets pour les stocker.

Pour utiliser les valeurs, redéfinissez ensuite les bits dans un long et vous pouvez utiliser la valeur.

Ensuite, votre RAM et le stockage de fichiers de la valeur sera de 40 bits (5 octets), MAIS vous devez considérer l'alignement des données si vous prévoyez d'utiliser un struct pour contenir les 5 octets Faites-moi savoir si vous avez besoin de précisions sur ces implications de décalage de bits et d'alignement des données.

De même, vous pouvez utiliser les 64 bits long et hide autres valeurs (3 caractères peut-être) dans les 24 bits résiduels que vous ne souhaitez pas utiliser. Encore une fois - en utilisant le décalage de bits pour ajouter et supprimer les valeurs de 24 bits.

9
Grantly

Je suppose que

  1. c'est C, et
  2. vous avez besoin d'un grand tableau unique de nombres de 40 bits, et
  3. vous êtes sur une machine peu endienne, et
  4. votre machine est suffisamment intelligente pour gérer l'alignement
  5. vous avez défini la taille comme étant le nombre de nombres 40 bits dont vous avez besoin

unsigned char hugearray[5*size+3];  // +3 avoids overfetch of last element

__int64 get_huge(unsigned index)
{
    __int64 t;
    t = *(__int64 *)(&hugearray[index*5]);
    if (t & 0x0000008000000000LL)
        t |= 0xffffff0000000000LL;
    else
        t &= 0x000000ffffffffffLL;
    return t;
}

void set_huge(unsigned index, __int64 value)
{
    unsigned char *p = &hugearray[index*5];
    *(long *)p = value;
    p[4] = (value >> 32);
}

Il peut être plus rapide de gérer le get avec deux quarts de travail.

__int64 get_huge(unsigned index)
{
    return (((*(__int64 *)(&hugearray[index*5])) << 24) >> 24);
}
6
cmm

Une autre variante qui pourrait être utile serait d'utiliser une structure:

typedef struct TRIPLE_40 {
  uint32_t low[3];
  uint8_t hi[3];
  uint8_t padding;
};

Une telle structure prendrait 16 octets et, si elle était alignée sur 16 octets, cadrerait entièrement dans une seule ligne de cache. Bien que l'identification des parties de la structure à utiliser puisse être plus coûteuse que si la structure contenait quatre éléments au lieu de trois, l'accès à une ligne de cache peut être beaucoup moins cher que l'accès à deux. Si les performances sont importantes, il convient d'utiliser certaines références, car certaines machines peuvent effectuer une opération divmod-3 à moindre coût et avoir un coût élevé par extraction de ligne de cache, tandis que d'autres peuvent avoir un accès mémoire moins cher et divmod-3 plus cher.

6
supercat

Dans le cas du stockage de plusieurs milliards d'entiers signés 40 bits et en supposant des octets 8 bits, vous pouvez compresser 8 entiers signés 40 bits dans une structure (dans le code ci-dessous en utilisant un tableau d'octets pour ce faire), et, puisque cette structure est généralement alignée, vous pouvez ensuite créer un tableau logique de ces groupes compressés et fournir une indexation séquentielle ordinaire de cela:

#include <limits.h>     // CHAR_BIT
#include <stdint.h>     // int64_t
#include <stdlib.h>     // div, div_t, ptrdiff_t
#include <vector>       // std::vector

#define STATIC_ASSERT( e ) static_assert( e, #e )

namespace cppx {
    using Byte = unsigned char;
    using Index = ptrdiff_t;
    using Size = Index;

    // For non-negative values:
    auto roundup_div( const int64_t a, const int64_t b )
        -> int64_t
    { return (a + b - 1)/b; }

}  // namespace cppx

namespace int40 {
    using cppx::Byte;
    using cppx::Index;
    using cppx::Size;
    using cppx::roundup_div;
    using std::vector;

    STATIC_ASSERT( CHAR_BIT == 8 );
    STATIC_ASSERT( sizeof( int64_t ) == 8 );

    const int bits_per_value    = 40;
    const int bytes_per_value   = bits_per_value/8;

    struct Packed_values
    {
        enum{ n = sizeof( int64_t ) };
        Byte bytes[n*bytes_per_value];

        auto value( const int i ) const
            -> int64_t
        {
            int64_t result = 0;
            for( int j = bytes_per_value - 1; j >= 0; --j )
            {
                result = (result << 8) | bytes[i*bytes_per_value + j];
            }
            const int64_t first_negative = int64_t( 1 ) << (bits_per_value - 1);
            if( result >= first_negative )
            {
                result = (int64_t( -1 ) << bits_per_value) | result;
            }
            return result;
        }

        void set_value( const int i, int64_t value )
        {
            for( int j = 0; j < bytes_per_value; ++j )
            {
                bytes[i*bytes_per_value + j] = value & 0xFF;
                value >>= 8;
            }
        }
    };

    STATIC_ASSERT( sizeof( Packed_values ) == bytes_per_value*Packed_values::n );

    class Packed_vector
    {
    private:
        Size                    size_;
        vector<Packed_values>   data_;

    public:
        auto size() const -> Size { return size_; }

        auto value( const Index i ) const
            -> int64_t
        {
            const auto where = div( i, Packed_values::n );
            return data_[where.quot].value( where.rem );
        }

        void set_value( const Index i, const int64_t value ) 
        {
            const auto where = div( i, Packed_values::n );
            data_[where.quot].set_value( where.rem, value );
        }

        Packed_vector( const Size size )
            : size_( size )
            , data_( roundup_div( size, Packed_values::n ) )
        {}
    };

}    // namespace int40

#include <iostream>
auto main() -> int
{
    using namespace std;

    cout << "Size of struct is " << sizeof( int40::Packed_values ) << endl;
    int40::Packed_vector values( 25 );
    for( int i = 0; i < values.size(); ++i )
    {
        values.set_value( i, i - 10 );
    }
    for( int i = 0; i < values.size(); ++i )
    {
        cout << values.value( i ) << " ";
    }
    cout << endl;
}
5

Si vous devez gérer des milliards d'entiers, j'essaierais d'encapsuler des tableaux de nombres de 40 bits au lieu de nombres simples 40 bits. De cette façon, vous pouvez tester différentes implémentations de tableaux (par exemple, une implémentation qui compresse les données à la volée, ou peut-être une qui stocke les données moins utilisées sur le disque.) Sans modifier le reste de votre code.

Voici un exemple d'implémentation ( http://rextester.com/SVITH57679 ):

class Int64Array
{
    char* buffer;
public:
    static const int BYTE_PER_ITEM = 5;

    Int64Array(size_t s)
    {
        buffer=(char*)malloc(s*BYTE_PER_ITEM);
    }
    ~Int64Array()
    {
        free(buffer);
    }

    class Item
    {
        char* dataPtr;
    public:
        Item(char* dataPtr) : dataPtr(dataPtr){}

        inline operator int64_t()
        {
            int64_t value=0;
            memcpy(&value, dataPtr, BYTE_PER_ITEM); // Assumes little endian byte order!
            return value;
        }

        inline Item& operator = (int64_t value)
        {
            memcpy(dataPtr, &value, BYTE_PER_ITEM); // Assumes little endian byte order!
            return *this;
        }
    };   

    inline Item operator[](size_t index) 
    {
        return Item(buffer+index*BYTE_PER_ITEM);
    }
};

Remarque: La conversion memcpy- de 40 bits à 64 bits est un comportement fondamentalement indéfini, car elle suppose une faible endianité. Cela devrait cependant fonctionner sur les plates-formes x86.

Note 2: Évidemment, il s'agit d'un code de preuve de concept, pas d'un code prêt pour la production. Pour l'utiliser dans de vrais projets, il faudrait ajouter (entre autres):

  • gestion des erreurs (malloc peut échouer!)
  • constructeur de copie (par exemple en copiant des données, en ajoutant un comptage de références ou en rendant le constructeur de copie privé)
  • déplacer le constructeur
  • surcharges const
  • Itérateurs compatibles STL
  • vérification des limites pour les index (dans la version de débogage)
  • la plage vérifie les valeurs (dans la version de débogage)
  • affirme pour les hypothèses implicites (peu d'endianisme)
  • En l'état, Item a une sémantique de référence, pas une sémantique de valeur, ce qui est inhabituel pour operator[]; Vous pouvez probablement contourner cela avec des astuces de conversion de type C++ intelligentes

Tout cela devrait être simple pour un programmeur C++, mais ils rendraient l'exemple de code beaucoup plus long sans le rendre plus clair, j'ai donc décidé de les omettre.

5
Niki

Oui, vous pouvez le faire, et cela économisera de l'espace pour de grandes quantités de nombres

Vous avez besoin d'une classe qui contient un vecteur std :: d'un type entier non signé.

Vous aurez besoin de fonctions membres pour stocker et récupérer un entier. Par exemple, si vous souhaitez stocker 64 entiers de 40 bits chacun, utilisez un vecteur de 40 entiers de 64 bits chacun. Ensuite, vous avez besoin d'une méthode qui stocke un entier avec index dans [0,64] et une méthode pour récupérer un tel entier.

Ces méthodes exécuteront certaines opérations de décalage, ainsi que certaines opérations binaires | et & .

Je n'ajoute pas plus de détails ici car votre question n'est pas très précise. Savez-vous combien d'entiers vous souhaitez stocker? Le savez-vous pendant la compilation? Le savez-vous quand le programme démarre? Comment les entiers doivent-ils être organisés? Comme un tableau? Comme une carte? Vous devez savoir tout cela avant d'essayer de compresser les entiers dans moins de stockage.

3
Hans Klünder

Il y a pas mal de réponses ici concernant l'implémentation, donc j'aimerais parler d'architecture.

Nous étendons généralement les valeurs 32 bits à des valeurs 64 bits pour éviter les débordements car nos architectures sont conçues pour gérer les valeurs 64 bits.

La plupart des architectures sont conçues pour fonctionner avec des entiers dont la taille est une puissance de 2 car cela rend le matériel beaucoup plus simple. Les tâches telles que la mise en cache sont beaucoup plus simples de cette façon: il existe un grand nombre d'opérations de division et de module qui peuvent être remplacées par un masquage de bits et des décalages si vous vous en tenez aux puissances de 2.

Comme exemple de combien cela importe, la spécification C++ 11 définit des cas de course multithreading basés sur des "emplacements de mémoire". Un emplacement mémoire est défini en 1.7.3:

Un emplacement de mémoire est soit un objet de type scalaire, soit une séquence maximale de champs binaires adjacents ayant tous une largeur non nulle.

En d'autres termes, si vous utilisez les champs binaires de C++, vous devez faire tout votre multithreading avec soin. Deux champs de bits adjacents doivent être traités comme le même emplacement mémoire, même si vous souhaitez que les calculs entre eux puissent être répartis sur plusieurs threads. C'est très inhabituel pour C++, donc susceptible de provoquer la frustration des développeurs si vous devez vous en soucier.

La plupart des processeurs ont une architecture de mémoire qui récupère des blocs de mémoire 32 bits ou 64 bits à la fois. Ainsi, l'utilisation de valeurs de 40 bits aura un nombre surprenant d'accès à la mémoire supplémentaire, affectant considérablement l'exécution. Considérez les problèmes d'alignement:

40-bit Word to access:   32-bit accesses   64bit-accesses
Word 0: [0,40)           2                 1
Word 1: [40,80)          2                 2
Word 2: [80,120)         2                 2
Word 3: [120,160)        2                 2
Word 4: [160,200)        2                 2
Word 5: [200,240)        2                 2
Word 6: [240,280)        2                 2
Word 7: [280,320)        2                 1

Sur une architecture 64 bits, un mot sur quatre sera "vitesse normale". Le reste nécessitera de récupérer deux fois plus de données. Si vous manquez beaucoup de cache, cela pourrait nuire aux performances. Même si vous obtenez des hits de cache, vous devrez décompresser les données et les reconditionner dans un registre 64 bits pour les utiliser (ce qui pourrait même impliquer une branche difficile à prévoir).

Il est tout à fait possible que cela en vaille le coût

Il y a des situations où ces sanctions sont acceptables. Si vous avez une grande quantité de données résidant en mémoire qui est bien indexée, vous pouvez trouver que les économies de mémoire valent la peine de performances. Si vous effectuez un grand nombre de calculs sur chaque valeur, vous constaterez peut-être que les coûts sont minimes. Si c'est le cas, n'hésitez pas à mettre en œuvre l'une des solutions ci-dessus. Cependant, voici quelques recommandations.

  • N'utilisez pas de champs de bits, sauf si vous êtes prêt à payer leur coût. Par exemple, si vous disposez d'un tableau de champs binaires et que vous souhaitez le diviser pour le traiter sur plusieurs threads, vous êtes bloqué. Selon les règles de C++ 11, les champs binaires forment tous un emplacement mémoire, donc ne peuvent être accédés que par un thread à la fois (c'est parce que la méthode de compression des champs binaires est définie par l'implémentation , donc C++ 11 ne peut pas vous aider à les distribuer d'une manière non définie par l'implémentation)
  • N'utilisez pas une structure contenant un entier 32 bits et un caractère pour faire 40 octets. La plupart des processeurs appliqueront l'alignement et vous ne sauvegarderez pas un seul octet.
  • Utilisez des structures de données homogènes, comme un tableau de caractères ou un tableau d'entiers 64 bits. Il est de loin plus facile d'obtenir l'alignement correct. (Et vous gardez également le contrôle de l'emballage, ce qui signifie que vous pouvez diviser un tableau en plusieurs threads pour le calcul si vous êtes prudent)
  • Concevez des solutions distinctes pour les processeurs 32 bits et 64 bits, si vous devez prendre en charge les deux plates-formes. Parce que vous faites quelque chose de très bas niveau et très mal pris en charge, vous devrez personnaliser chaque algorithme en fonction de son architecture de mémoire.
  • N'oubliez pas que la multiplication des nombres de 40 bits est différente de la multiplication des extensions de 64 bits des nombres de 40 bits réduite à 40 bits. Tout comme pour le FPU x87, vous devez vous rappeler que le regroupement de vos données entre les tailles de bits modifie votre résultat.
3
Cort Ammon

Cela demande la diffusion en continu de la compression sans perte en mémoire. S'il s'agit d'une application Big Data, les astuces d'emballage denses sont au mieux des solutions tactiques pour ce qui semble nécessiter un middleware assez décent ou une prise en charge au niveau du système. Ils auraient besoin de tests approfondis pour s'assurer que l'on est capable de récupérer tous les bits indemnes. Et les implications en termes de performances sont très non triviales et très dépendantes du matériel en raison d'interférences avec l'architecture de mise en cache du processeur (par exemple, lignes de cache vs structure de compression). Quelqu'un a mentionné des structures de maillage complexes: celles-ci sont souvent affinées pour coopérer avec des architectures de mise en cache particulières.

Il n'est pas clair d'après les exigences si l'OP a besoin d'un accès aléatoire. Compte tenu de la taille des données, il est plus probable que l'on n'ait besoin que d'un accès aléatoire local sur des segments relativement petits, organisés hiérarchiquement pour la récupération. Même le matériel le fait à de grandes tailles de mémoire (NUMA). Comme le montrent les formats de film sans perte, il devrait être possible d'obtenir un accès aléatoire en morceaux ("images") sans avoir à charger l'ensemble de données entier dans la mémoire chaude (à partir du magasin de sauvegarde en mémoire compressé).

Je connais un système de base de données rapide (kdb de KX Systems pour en nommer un, mais je sais qu'il y en a d'autres) qui peut gérer des ensembles de données extrêmement volumineux en mappant apparemment en mémoire de grands ensembles de données à partir du magasin de sauvegarde. Il a la possibilité de compresser et d'étendre les données à la volée de manière transparente.

3
GuyB

Si ce que vous voulez vraiment, c'est un tableau d'entiers 40 bits (que vous ne pouvez évidemment pas avoir), je combinerais simplement un tableau de 32 bits et un tableau d'entiers 8 bits.

Pour lire une valeur x à l'index i:

uint64_t x = (((uint64_t) array8 [i]) << 32) + array32 [i];

Pour écrire une valeur x dans l'index i:

array8 [i] = x >> 32; array32 [i] = x;

Évidemment bien encapsulé dans une classe en utilisant des fonctions en ligne pour une vitesse maximale.

Il y a une situation où cela n'est pas optimal, et c'est quand vous faites un accès vraiment aléatoire à de nombreux éléments, de sorte que chaque accès à un tableau int soit un échec de cache - ici, vous obtiendrez deux échecs de cache à chaque fois. Pour éviter cela, définissez une structure de 32 octets contenant un tableau de six uint32_t, un tableau de six uint8_t et deux octets inutilisés (41 2/3 bits par numéro); le code pour accéder à un élément est légèrement plus compliqué, mais les deux composants de l'élément sont dans la même ligne de cache.

2
gnasher729