D'abord, le titre. Vous n'avez pas besoin de l'arithmétique modulo pour envelopper le tampon si vous utilisez des bits ints pour contenir les "pointeurs" de tête et de queue, et si vous les dimensionnez de manière à ce qu'ils soient parfaitement synchronisés. IE : 4096 rempli dans un 12-bit unsigned int est 0 tout seul, non molesté en aucune façon. L'élimination de l'arithmétique modulo, même pour les puissances de 2, double la vitesse - presque exactement.
10 millions d'itérations de remplissage et de vidange d'un tampon de 4096 éléments de données de tout type prennent 52 secondes sur mon Dell XPS 8500 de 3e génération i7 en utilisant le compilateur C++ de Visual Studio 2010 avec l'inlining par défaut, et 1/8192e de ce temps pour servir une donnée.
Je réécrirais les boucles de test dans main() pour qu'elles ne contrôlent plus le flux - qui est, et devrait être, contrôlé par les valeurs de retour indiquant que le tampon est plein ou vide, et les instructions break ; correspondantes. IE : le remplisseur et le vidangeur devraient pouvoir se cogner l'un contre l'autre sans corruption ou instabilité. A un moment donné, j'espère pouvoir utiliser ce code en multithread, ce qui rendra ce comportement crucial.
Le QUEUE_DESC (descripteur de file d'attente) et la fonction d'initialisation obligent tous les tampons dans ce code à être une puissance de 2. Le schéma ci-dessus ne fonctionnera PAS autrement. A ce propos, notez que QUEUE_DESC n'est pas codé en dur, il utilise une constante manifeste (#define BITS_ELE_KNT) pour sa construction. (Je suppose qu'une puissance de 2 est une flexibilité suffisante ici).
Pour rendre la taille du tampon sélectionnable en cours d'exécution, j'ai essayé différentes approches (non montrées ici), et me suis contenté d'utiliser des USHRT pour Head, Tail, EleKnt capables de gérer un tampon FIFO [USHRT]. Pour éviter l'arithmétique modulo, j'ai créé un masque pour && avec Head, Tail, mais ce masque s'avère être (EleKnt -1), donc utilisez simplement cela. L'utilisation de USHRTS au lieu de bit ints a augmenté les performances de ~ 15% sur une machine silencieuse. Les cœurs des CPU Intel ont toujours été plus rapides que leurs bus, donc sur une machine occupée et partagée, l'empaquetage de vos structures de données vous permet d'être chargé et d'exécuter avant les autres threads en compétition. Compromis.
Notez que le stockage réel du tampon est alloué sur le tas avec calloc(), et le pointeur est à la base de la structure, donc la structure et le pointeur ont EXACTEMENT la même adresse. IE ; aucun offset ne doit être ajouté à l'adresse de la structure pour bloquer les registres.
Dans le même ordre d'idées, toutes les variables liées à la gestion du tampon sont physiquement adjacentes au tampon, liées à la même structure, de sorte que le compilateur peut créer un magnifique langage d'assemblage. Il faut tuer l'optimisation inline pour voir de l'assembleur, car sinon il est écrasé dans l'oubli.
Pour supporter le polymorphisme de tout type de données, j'ai utilisé memcpy() au lieu des assignations. Si vous avez seulement besoin de la flexibilité de supporter un type de variable aléatoire par compilation, alors ce code fonctionne parfaitement.
Pour le polymorphisme, il suffit de connaître le type et son besoin de stockage. Le tableau de descripteurs DATA_DESC permet de garder la trace de chaque donnée placée dans QUEUE_DESC.pBuffer afin de pouvoir la récupérer correctement. Je me contenterais d'allouer suffisamment de mémoire pBuffer pour contenir tous les éléments du plus grand type de données, mais je garderais la trace de la quantité de mémoire qu'une donnée donnée utilise réellement dans DATA_DESC.dBytes. L'alternative est de réinventer un gestionnaire de tas.
Cela signifie que le UCHAR *pBuffer de QUEUE_DESC aurait un tableau compagnon parallèle pour garder la trace du type et de la taille des données, tandis que l'emplacement de stockage d'une donnée dans pBuffer resterait tel qu'il est actuellement. Le nouveau membre serait quelque chose comme DATA_DESC *pDataDesc, ou, peut-être, DATA_DESC DataDesc[2^BITS_ELE_KNT] si vous pouvez trouver un moyen de battre votre compilateur en soumission avec une telle référence directe. Calloc() est toujours plus flexible dans ces situations.
Vous auriez toujours memcpy() dans Q_Put(),Q_Get, mais le nombre d'octets réellement copiés serait déterminé par DATA_DESC.dBytes, et non par QUEUE_DESC.EleBytes. Les éléments sont potentiellement tous de types/tailles différents pour tout put ou get donné.
Je pense que ce code répond aux exigences de vitesse et de taille de la mémoire tampon, et qu'il peut être adapté pour répondre à l'exigence de 6 types de données différents. J'ai laissé les nombreux tests, sous forme d'instructions printf(), afin que vous puissiez vous assurer (ou non) que le code fonctionne correctement. Le générateur de nombres aléatoires démontre que le code fonctionne pour n'importe quelle combinaison tête/queue aléatoire.
enter code here
// Queue_Small.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include <stdio.h>
#include <time.h>
#include <limits.h>
#include <stdlib.h>
#include <malloc.h>
#include <memory.h>
#include <math.h>
#define UCHAR unsigned char
#define ULONG unsigned long
#define USHRT unsigned short
#define dbl double
/* Queue structure */
#define QUEUE_FULL_FLAG 1
#define QUEUE_EMPTY_FLAG -1
#define QUEUE_OK 0
//
#define BITS_ELE_KNT 12 //12 bits will create 4.096 elements numbered 0-4095
//
//typedef struct {
// USHRT dBytes:8; //amount of QUEUE_DESC.EleBytes storage used by datatype
// USHRT dType :3; //supports 8 possible data types (0-7)
// USHRT dFoo :5; //unused bits of the unsigned short host's storage
// } DATA_DESC;
// This descriptor gives a home to all the housekeeping variables
typedef struct {
UCHAR *pBuffer; // pointer to storage, 16 to 4096 elements
ULONG Tail :BITS_ELE_KNT; // # elements, with range of 0-4095
ULONG Head :BITS_ELE_KNT; // # elements, with range of 0-4095
ULONG EleBytes :8; // sizeof(elements) with range of 0-256 bytes
// some unused bits will be left over if BITS_ELE_KNT < 12
USHRT EleKnt :BITS_ELE_KNT +1;// 1 extra bit for # elements (1-4096)
//USHRT Flags :(8*sizeof(USHRT) - BITS_ELE_KNT +1); // flags you can use
USHRT IsFull :1; // queue is full
USHRT IsEmpty :1; // queue is empty
USHRT Unused :1; // 16th bit of USHRT
} QUEUE_DESC;
// ---------------------------------------------------------------------------
// Function prototypes
QUEUE_DESC *Q_Init(QUEUE_DESC *Q, int BitsForEleKnt, int DataTypeSz);
int Q_Put(QUEUE_DESC *Q, UCHAR *pNew);
int Q_Get(UCHAR *pOld, QUEUE_DESC *Q);
// ---------------------------------------------------------------------------
QUEUE_DESC *Q_Init(QUEUE_DESC *Q, int BitsForEleKnt, int DataTypeSz) {
memset((void *)Q, 0, sizeof(QUEUE_DESC));//init flags and bit integers to zero
//select buffer size from powers of 2 to receive modulo
// arithmetic benefit of bit uints overflowing
Q->EleKnt = (USHRT)pow(2.0, BitsForEleKnt);
Q->EleBytes = DataTypeSz; // how much storage for each element?
// Randomly generated head, tail a test fixture only.
// Demonstrates that the queue can be entered at a random point
// and still perform properly. Normally zero
srand(unsigned(time(NULL))); // seed random number generator with current time
Q->Head = Q->Tail = rand(); // supposed to be set to zero here, or by memset
Q->Head = Q->Tail = 0;
// allocate queue's storage
if(NULL == (Q->pBuffer = (UCHAR *)calloc(Q->EleKnt, Q->EleBytes))) {
return NULL;
} else {
return Q;
}
}
// ---------------------------------------------------------------------------
int Q_Put(QUEUE_DESC *Q, UCHAR *pNew)
{
memcpy(Q->pBuffer + (Q->Tail * Q->EleBytes), pNew, Q->EleBytes);
if(Q->Tail == (Q->Head + Q->EleKnt)) {
// Q->IsFull = 1;
Q->Tail += 1;
return QUEUE_FULL_FLAG; // queue is full
}
Q->Tail += 1; // the unsigned bit int MUST wrap around, just like modulo
return QUEUE_OK; // No errors
}
// ---------------------------------------------------------------------------
int Q_Get(UCHAR *pOld, QUEUE_DESC *Q)
{
memcpy(pOld, Q->pBuffer + (Q->Head * Q->EleBytes), Q->EleBytes);
Q->Head += 1; // the bit int MUST wrap around, just like modulo
if(Q->Head == Q->Tail) {
// Q->IsEmpty = 1;
return QUEUE_EMPTY_FLAG; // queue Empty - nothing to get
}
return QUEUE_OK; // No errors
}
//
// ---------------------------------------------------------------------------
int _tmain(int argc, _TCHAR* argv[]) {
// constrain buffer size to some power of 2 to force faux modulo arithmetic
int LoopKnt = 1000000; // for benchmarking purposes only
int k, i=0, Qview=0;
time_t start;
QUEUE_DESC Queue, *Q;
if(NULL == (Q = Q_Init(&Queue, BITS_ELE_KNT, sizeof(int)))) {
printf("\nProgram failed to initialize. Aborting.\n\n");
return 0;
}
start = clock();
for(k=0; k<LoopKnt; k++) {
//printf("\n\n Fill'er up please...\n");
//Q->Head = Q->Tail = rand();
for(i=1; i<= Q->EleKnt; i++) {
Qview = i*i;
if(QUEUE_FULL_FLAG == Q_Put(Q, (UCHAR *)&Qview)) {
//printf("\nQueue is full at %i \n", i);
//printf("\nQueue value of %i should be %i squared", Qview, i);
break;
}
//printf("\nQueue value of %i should be %i squared", Qview, i);
}
// Get data from queue until completely drained (empty)
//
//printf("\n\n Step into the lab, and see what's on the slab... \n");
Qview = 0;
for(i=1; i; i++) {
if(QUEUE_EMPTY_FLAG == Q_Get((UCHAR *)&Qview, Q)) {
//printf("\nQueue value of %i should be %i squared", Qview, i);
//printf("\nQueue is empty at %i", i);
break;
}
//printf("\nQueue value of %i should be %i squared", Qview, i);
}
//printf("\nQueue head value is %i, tail is %i\n", Q->Head, Q->Tail);
}
printf("\nQueue time was %5.3f to fill & drain %i element queue %i times \n",
(dbl)(clock()-start)/(dbl)CLOCKS_PER_SEC,Q->EleKnt, LoopKnt);
printf("\nQueue head value is %i, tail is %i\n", Q->Head, Q->Tail);
getchar();
return 0;
}
1 votes
Avez-vous besoin d'un tampon circulaire ou d'une file d'attente ? Les opérations requises font penser à une file d'attente. J'admets qu'avec l'exigence d'une taille fixe, l'utilisation d'un tampon circulaire est logique, mais je ne suis pas sûr que le titre de la question reflète votre question réelle.
1 votes
Je suis ouvert à d'autres structures de données si vous pensez qu'elles peuvent être plus rapides, mais je suis raisonnablement certain qu'un tampon circulaire fixe en mémoire sera plus performant que malloc/free des éléments de la file d'attente. Bien que je suppose que je dois faire malloc/free de la charge utile de toute façon : si je pouvais faire un seul malloc pour l'élément et la charge utile, ça pourrait valoir le coup.
0 votes
"si vous pensez qu'ils peuvent être plus rapides" ? - Je pense qu'il faut faire des tests. Au fait, qu'entendez-vous par "très hautes performances" ?
1 votes
Je vais évaluer toutes les idées (j'ai des capacités de génération de données de test basées sur le débit réel). Les "hautes performances" seront tout ce qui permet à l'unité centrale actuelle de gérer cette nouvelle charge récemment augmentée que le client a jugé bon de lui infliger :-)
2 votes
Je vais clarifier ça. Je n'ai pas besoin des performances d'un processeur quadruple dernier cri d'Intel. Il fonctionne sur une variante 8051 qui n'est pas la plus rapide, donc je cherche simplement des idées d'optimisation à tester. Si aucune d'entre elles ne fonctionne, le client devra fabriquer un nouveau matériel basé sur un autre processeur et cela ne sera pas bon marché. La gestion des articles dans les files d'attente actuelles a été identifiée comme le principal goulot d'étranglement.