Question

J'ai posé une question sur les tailles de type C Je reçois une bonne réponse, mais je me suis rendu compte que je ne formulerais peut-être pas très bien la question pour être utile à mes fins.

Mon expérience était celle d'un ingénieur en informatique avant de passer à un ingénieur en logiciel, donc j'aime les architectures informatiques et je pense toujours à la création de VM. Je viens de terminer un projet intéressant de création de machines virtuelles sous Java dont je suis assez fier. Mais il y a quelques problèmes juridiques que je ne peux pas ouvrir maintenant et j'ai actuellement un peu de temps libre. Je veux donc voir si je peux créer une autre machine virtuelle sur C (avec une vitesse plus rapide) juste pour le plaisir et pour l’éducation.

Le problème, c’est que je ne suis pas un programme C la dernière fois que j’ai écrit un problème non trivial en C était il ya plus de 10 ans. J'étais Pascal, Delphi et maintenant programmeur Java et PHP.

Il y a un certain nombre d'obstacles que je peux prévoir et j'essaie de m'en attaquer et qui consiste à accéder à une bibliothèque existante (en Java, la réflexion résout ce problème).

Je prévois de résoudre ce problème en ayant un tampon de données (similaire à stack). Le client de ma machine virtuelle peut programmer pour mettre des données dans ces piles avant de me donner le pointeur sur la fonction native.

int main(void) {
    // Prepare stack
    int   aStackSize = 1024*4;
    char *aStackData = malloc(aStackSize);

    // Initialise stack
    VMStack aStack;
    VMStack_Initialize(&aStack, (char *)aStackData, aStackSize);

    // Push in the parameters
    char *Params = VMStack_CurrentPointer(&aStack);
    VMStack_Push_int   (&aStack, 10  ); // Push an int
    VMStack_Push_double(&aStack, 15.3); // Push a double

    // Prepare space for the expected return
    char *Result = VMStack_CurrentPointer(&aStack);
    VMStack_Push_double(&aStack, 0.0); // Push an empty double for result

    // Execute
    void (*NativeFunction)(char*, char*) = &Plus;
    NativeFunction(Params, Result); // Call the function

    // Show the result
    double ResultValue = VMStack_Pull_double(&aStack); // Get the result
    printf("Result:  %5.2f\n", ResultValue);               // Print the result

    // Remove the previous parameters
    VMStack_Pull_double(&aStack); // Pull to clear space of the parameter
    VMStack_Pull_int   (&aStack); // Pull to clear space of the parameter

    // Just to be sure, print out the pointer and see if it is `0`
    printf("Pointer: %d\n", aStack.Pointer);

    free(aStackData);
    return EXIT_SUCCESS;
}

Les fonctions d'envoi, d'extraction et d'appel de la fonction native peuvent être déclenchées par un code d'octet (c'est ainsi que la machine virtuelle sera créée ultérieurement).

Par souci d’exhaustivité (afin que vous puissiez l’essayer sur votre ordinateur), voici le code correspondant à Stack:

typedef struct {
    int  Pointer;
    int  Size;
    char *Data;
} VMStack;

inline void   VMStack_Initialize(VMStack *pStack, char *pData, int pSize) __attribute__((always_inline));
inline char   *VMStack_CurrentPointer(VMStack *pStack)                    __attribute__((always_inline));
inline void   VMStack_Push_int(VMStack *pStack, int pData)                __attribute__((always_inline));
inline void   VMStack_Push_double(VMStack *pStack, double pData)          __attribute__((always_inline));
inline int    VMStack_Pull_int(VMStack *pStack)                           __attribute__((always_inline));
inline double VMStack_Pull_double(VMStack *pStack)                        __attribute__((always_inline));

inline void VMStack_Initialize(VMStack *pStack, char *pData, int pSize) {
    pStack->Pointer = 0;
    pStack->Data    = pData;
    pStack->Size    = pSize;
}

inline char *VMStack_CurrentPointer(VMStack *pStack) {
    return (char *)(pStack->Pointer + pStack->Data);
}

inline void VMStack_Push_int(VMStack *pStack, int pData) {
    *(int *)(pStack->Data + pStack->Pointer) = pData;
    pStack->Pointer += sizeof pData; // Should check the overflow
}
inline void VMStack_Push_double(VMStack *pStack, double pData) {
    *(double *)(pStack->Data + pStack->Pointer) = pData;
    pStack->Pointer += sizeof pData; // Should check the overflow
}

inline int VMStack_Pull_int(VMStack *pStack) {
    pStack->Pointer -= sizeof(int);// Should check the underflow
    return *((int *)(pStack->Data + pStack->Pointer));
}
inline double VMStack_Pull_double(VMStack *pStack) {
    pStack->Pointer -= sizeof(double);// Should check the underflow
    return *((double *)(pStack->Data + pStack->Pointer));
}

Du côté des fonctions natives, j'ai créé les éléments suivants à des fins de test:

// These two structures are there so that Plus will not need to access its parameter using
//    arithmetic-pointer operation (to reduce mistake and hopefully for better speed).
typedef struct {
    int    A;
    double B;
} Data;
typedef struct {
    double D;
} DDouble;

// Here is a helper function for displaying void PrintData(Data *pData, DDouble *pResult) { printf("%5.2f + %5.2f = %5.2f\n", pData->A*1.0, pData->B, pResult->D); }

// Some native function void Plus(char* pParams, char* pResult) { Data *D = (Data *)pParams; // Access data without arithmetic-pointer operation DDouble *DD = (DDouble *)pResult; // Same for return DD->D = D->A + D->B; PrintData(D, DD); }

Une fois exécuté, le code ci-dessus renvoie:

10.00 + 15.30 = 25.30
Result:  25.30
Pointer: 0

Cela fonctionne bien sur ma machine (Linux x86 32 bits GCC-C99). Ce sera très bien si cela fonctionne aussi sur d'autres OS / Architecture. Mais il y a au moins trois problèmes liés à la mémoire dont nous devons être conscients.

1). Taille des données - Il semble que si je compile à la fois la machine virtuelle et les fonctions natives à l'aide du même compilateur sur la même architecture, les types de taille doivent être identiques.

2). Endianness - Identique à la taille des données.

3). Alignement de la mémoire - Quel est le problème car des octets de remplissage peuvent être ajoutés dans la structure, mais il est difficile de les synchroniser lors de la préparation de la pile de paramètres (il n'y a aucun moyen de savoir comment le remplissage est ajouté à l'exception du codage strict).

Mes questions sont les suivantes:

1). Si je connais la taille des types, existe-t-il un moyen de modifier les fonctions push et pull pour les synchroniser exactement avec le remplissage de la structure? (modifier pour laisser le compilateur s'en charger, comme les problèmes Datasize et Endians).

2). Si je compresse la structure par un (avec #pragma pack(1)); (2.1) La pénalité de performance sera-t-elle acceptable? et (2.2) La stabilité du programme sera-t-elle menacée?

3). Que diriez-vous d'un rembourrage de 2,4 ou 8? Ce qui devrait être bon pour le système général 32 ou 64 bits?

4). Pouvez-vous me guider vers une documentation pour un algorithme de remplissage exact, par exemple pour GCC sur x86?

5). Y a-t-il un meilleur moyen?

REMARQUE: multiplate-forme, ce n’est pas mon but ultime, mais je ne peux pas résister. En outre, la performance n’est pas ma cible dès lors que ce n’est pas si moche. Tout cela est pour le plaisir et l'apprentissage.

Désolé pour mon anglais et le très long message.

Merci à tous d'avance.

Était-ce utile?

La solution

Commentaires tangentiels

Ces premiers éléments sont tangents aux questions que vous avez posées, mais ...

// Execute
void (*NativeFunction)(char*, char*) = &Plus;
NativeFunction(Params, Result); // Call the function

Je pense que vous devriez probablement utiliser "void *" au lieu de "char *" ici. J'aurais aussi un typedef pour le type de pointeur de fonction:

typedef void (*Operator)(void *params, void *result);

Ensuite, vous pouvez écrire:

Operator NativeFunction = Plus;

La fonction réelle serait également modifiée - mais très peu:

void Plus(void *pParams, void *pResult)

Vous avez également un problème de dénomination mineur: il s’agit de la fonction "IntPlusDoubleGivesDouble ()", plutôt que de la fonction générale "ajouter des types quelconques".

Réponses directes aux questions

  

1). Si je connais la taille des types, existe-t-il un moyen de modifier les fonctions push et pull pour les synchroniser exactement avec le remplissage de la structure? (modifier pour laisser le compilateur s'en charger, comme les problèmes Datasize et Endians).

Il n’ya pas de moyen facile de le faire. Par exemple, considérons:

struct Type1
{
     unsigned char byte;
     int           number;
};
struct Type2
{
     unsigned char byte;
     double        number;
};

Sur certaines architectures (SPARC 32 bits ou 64 bits, par exemple), la structure Type1 aura un "numéro" aligné sur une limite de 4 octets, mais la structure Type2 aura un "numéro" aligné sur un 8 limite d'octet (et peut avoir un «long double» sur une limite de 16 octets). Votre stratégie "pousser des éléments individuels" frapperait le pointeur de pile de 1 après avoir poussé la valeur "octet" - vous voudriez donc déplacer le pointeur de pile de 3 ou 7 avant de pousser le "nombre", si le pointeur de pile n'est pas déjà approprié aligné. Une partie de la description de votre machine virtuelle sera constituée des alignements requis pour tout type donné. le code push correspondant devra garantir le bon alignement avant d'appuyer.

  

2). Si je compresse la structure par un (en utilisant #pragma pack (1)); (2.1) La pénalité de performance sera-t-elle acceptable? et (2.2) La stabilité du programme sera-t-elle menacée?

Sur les machines x86 et x86_64, si vous compressez les données, les performances de l'accès aux données mal alignées seront pénalisées. Sur des machines telles que SPARC ou PowerPC (selon mecki ), vous obtiendrez une erreur de bus ou quelque chose de similaire à la place - vous devez accéder aux données correctement alignées. Vous économiserez peut-être un peu d’espace mémoire, tout en réduisant les performances. Vous feriez mieux de garantir les performances (ce qui inclut ici «effectuer correctement au lieu de planter») au coût marginal dans l'espace.

  

3). Que diriez-vous d'un rembourrage de 2,4 ou 8? Ce qui devrait être bon pour le système général 32 ou 64 bits?

Sous SPARC, vous devez associer un type de base à N octets à une limite de N octets. Sur x86, vous obtiendrez de meilleures performances si vous faites la même chose.

  

4). Pouvez-vous me guider vers une documentation pour un algorithme de remplissage exact, disons pour GCC sur x86?

Il vous faudra lire le manuel .

  

5). Y a-t-il un meilleur moyen?

Notez que l'astuce 'Type1' avec un seul caractère suivi d'un type vous donne l'exigence d'alignement - en utilisant éventuellement la macro 'offsetof ()' de <stddef.h>:

offsetof(struct Type1, number)

Eh bien, je ne mettrais pas les données sur la pile. Je travaillerais avec l'alignement natif car il est configuré pour offrir les meilleures performances. Le rédacteur du compilateur n'ajoute pas de remplissage à une structure; ils l'ont mis là parce que cela fonctionne «mieux» pour l'architecture. Si vous décidez de mieux vous connaître, vous pouvez vous attendre aux conséquences habituelles: des programmes plus lents, qui échouent parfois et ne sont pas aussi portables.

Je ne suis pas non plus convaincu d'écrire le code dans les fonctions d'opérateur pour supposer que la pile contenait une structure. Je tirerais les valeurs de la pile via l'argument Params, sachant quels étaient les bons décalages et types. Si je poussais un entier et un double, je tirerais un entier et un double (ou, peut-être, dans l'ordre inverse - je tirerais un double et un int). Sauf si vous planifiez une VM inhabituelle, peu de fonctionss aura de nombreux arguments.

Autres conseils

Intéressant post et montre que vous avez mis beaucoup de travail. Presque le poste SO idéal.

Je n'ai pas de réponses prêtes, alors s'il vous plaît, supportez-moi. Je vais devoir poser quelques questions supplémentaires: P

  

1). Si je connais la taille des types, existe-t-il un moyen de modifier les fonctions push et pull pour les synchroniser exactement avec le remplissage de la structure? (modifier pour laisser le compilateur s'en charger, comme les problèmes Datasize et Endians).

Est-ce seulement du point de vue des performances? Envisagez-vous d'introduire des pointeurs ainsi que des types arithmétiques natifs?

  

2). Si je compresse la structure par un (en utilisant #pragma pack (1)); (2.1) La pénalité de performance sera-t-elle acceptable? et (2.2) La stabilité du programme sera-t-elle menacée?

Ceci est une chose définie par l'implémentation. Pas quelque chose sur lequel vous pouvez compter sur toutes les plateformes.

  

3). Que diriez-vous d'un rembourrage de 2,4 ou 8? Ce qui devrait être bon pour le système général 32 ou 64 bits?

La valeur qui correspond à la taille du mot natif doit vous donner des performances optimales.

  

4). Pouvez-vous me guider vers une documentation pour un algorithme de remplissage exact, par exemple pour GCC sur x86?

Je ne connais rien du haut de ma tête. Mais j'ai vu un code similaire à utilisé .

Notez que vous pouvez spécifiez les attributs des variables à l'aide de GCC (qui contient également un élément appelé default_struct __attribute__((packed)) qui désactive le remplissage).

Il y a de très bonnes questions ici, beaucoup d’entre elles s’embrouilleront dans des problèmes de conception importants mais pour la plupart d’entre nous - nous pouvons voir l’objectif que vous êtes en train de travailler (directement posté au moment où j’écris afin que vous puissiez voir que vous générez Nous comprenons assez bien votre anglais que nous travaillons actuellement sur des problèmes de compilation et de conception de langage. Il est difficile de résoudre le problème, mais si vous travaillez déjà dans JNI, vous pouvez espérer ...

D'une part, j'essayais de m'éloigner des pragmas; Beaucoup de gens, très nombreux ne seront pas d’accord avec cela. Pour une discussion canonique des raisons, voir la justification de la position du langage D sur la question. Pour un autre, il y a un pointeur 16 bits enfoui dans votre code.

Les problèmes sont presque sans fin, bien étudiés et susceptibles de nous enterrer dans l’opposition et l’intransigeance intramurale. si je peux suggérer de lire la page d'accueil de Kenneth Louden ainsi que le manuel d'architecture intel. Je l'ai, j'ai essayé de le lire. L'alignement de la structure des données, ainsi que de nombreuses autres questions que vous avez abordées, est profondément ancré dans la science des compilateurs historiques et risque de vous faire perdre votre temps à savoir qui sait quoi. (argot ou idiomatique pour des questions de conséquence imprévisibles)

Cela étant dit, voici:

  1. tailles de type C Quelles tailles de caractères?
  2. Ingénieur en informatique avant de passer à Ingénieur logiciel Avez-vous déjà étudié les microcontrôleurs? Jetez un coup d’œil à certains travaux de Don Lancaster.
  3. Pascal, Delphi et maintenant Java et PHP programmeur. Celles-ci sont relativement éloignées de l'architecture fondamentale de base des processeurs, même si de nombreuses personnes montreront ou tenteront de montrer comment elles peuvent être utilisées pour écrire des routines puissantes et fondamentales. Je suggère de regarder l'analyseur de descente récursif de David Eck pour voir exactement comment commencer l'étude de la question. En outre, Kenneth Louden a implémenté & «Tiny &»; qui est un compilateur réel. J'ai trouvé quelque chose il n'y a pas si longtemps que je pense s'appelait asm dot org ... un travail très avancé et très puissant était disponible pour étudier là-bas, mais c'est une longue route pour commencer à écrire en assembleur dans le but de se lancer dans la science des compilateurs. De plus, la plupart des architectures ont des différences qui ne sont pas cohérentes d'un processeur à l'autre.
  4. accéder à la bibliothèque existante

Il y a beaucoup de bibliothèques autour, Java en a de bonnes. Je ne sais pas pour les autres. Une approche consiste à essayer d'écrire une bibliothèque. Java a une bonne base et laisse de la place aux gens qui aiment essayer de trouver quelque chose de mieux. Commencez par améliorer Knuth-Morris-Pratt ou quelque chose du genre: les lieux de départ ne manquent pas. Essayez Répertoire des algorithmes de programmation informatique et, bien sûr, consultez Dictionnaire des algorithmes et des structures de données sur NIST

  1. always_inline

Pas nécessairement, voir Dov Bulka - le travailleur est titulaire d’un doctorat en informatique et est également un auteur compétent dans les domaines où la rentabilité, la fiabilité, la robustesse, etc., ne sont pas soumis à une partie de la & société; modèle " paradigme d'où nous tirons une partie de & "Oh! cela n'a pas d'importance " sur des questions qui comptent réellement.

En guise de conclusion, l’instrumentation et le contrôle représentent plus de 60% du marché actuel des compétences en programmation accomplies, comme vous le décrivez. Pour une raison quelconque, nous entendons surtout parler du modèle commercial. Laissez-moi partager avec vous et à l'intérieur de la friandise je viens d'une source fiable. Entre 10% et 60% ou plus les risques réels pour la sécurité et les biens proviennent de problèmes de véhicules plutôt que de cambrioleur, vol, etc.ing. Vous ne recevrez jamais d'appel pour & "; 90 jours de minéraux de bustin dans les installations d'extraction de minerais du comté! &"; En ce qui concerne les contraventions, la plupart des gens ne réalisent même pas que les citations relatives au trafic sont des délits de classe 4 (N.A. - US.A.) et sont effectivement classifiables comme tels.

Il me semble que vous avez fait un bon pas en avant vers un travail de qualité, ...

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top