クロスプラットフォームVMのCメモリ管理
-
06-07-2019 - |
質問
Cタイプのサイズについて質問を尋ねましたかなり良い答えが得られますが、自分の目的に役立つように質問をうまく構成できない可能性があることに気付きました。
私のバックグラウンドは、ソフトウェアエンジニアに移る前はコンピューターエンジニアだったので、コンピューターアーキテクチャが好きで、常にVMの作成を考えています。 JavaでVMを作成する興味深いプロジェクトを終えたところです。しかし、私は現在それをオープンソース化することができないいくつかの法的問題があり、現在私にはいくらかの自由時間があります。そこで、楽しくて教育のためだけに、C上で別のVMを(より高速に)作成できるかどうかを確認したいのです。
問題は、私が10年以上前にトリビア以外のC問題を書いたとき、Cプログラムではないということです。私はPascal、Delphi、そして今はJavaとPHPプログラマでした。
予測できる障害は数多くあり、それらに対処しようとしていますが、既存のライブラリにアクセスしています(Javaでは、リフレクションがこの問題を解決します)。
データのバッファを作成することでこれを解決する予定です(スタックと同様)。 VMのクライアントは、ネイティブ関数へのポインターを与える前に、これらのスタックにデータを入れるようにプログラムできます。
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;
}
ネイティブ関数のプッシュ、プル、および呼び出しは、バイトコードによってトリガーできます(つまり、VMを後で作成する方法です)。
完全を期すため(マシンで試せるように)、ここに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));
}
ネイティブ関数側では、テスト目的で以下を作成しました:
// 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);
}
実行すると、上記のコードは以下を返します:
10.00 + 15.30 = 25.30
Result: 25.30
Pointer: 0
これは私のマシン(Linux x86 32ビットGCC-C99)でうまく機能します。これが他のOS / Architectureでも機能する場合、非常に便利です。しかし、少なくとも3つのメモリ関連の注意事項があります。注意してください。
1)。データサイズ-同じアーキテクチャで同じコンパイラを使用してVMとネイティブ関数の両方をコンパイルする場合、サイズタイプは同じである必要があります。
2)。エンディアンネス-データサイズと同じ。
3)。メモリアライメント-パディングバイトが構造体に追加される可能性があるため問題ですが、パラメータスタックを準備するときに同期するのは困難です(ハードコーディングを除いてパディングが追加される方法を知る方法はありません)。
私の質問:
1)。型のサイズがわかっている場合、プッシュおよびプル機能を変更して構造体のパディングと正確に同期する方法はありますか? (データサイズやエンディアンの問題のようにコンパイラーが処理できるように変更します。)
2)。構造を1つずつパックする場合(#pragma pack(1)
を使用); (2.1)パフォーマンスのペナルティは許容されますか? (2.2)プログラムの安定性は危険にさらされますか?
3)。 2、4、または8のパディングはどうですか?一般的な32ビットまたは64ビットシステムに適しているのはどれですか?
4)。 x86上のGCCについて言えば、正確なパディングアルゴリズムのドキュメントを教えてもらえますか?
5)。より良い方法はありますか?
注:クロスプラットフォームは私の最終的な目標ではありませんが、抵抗することはできません。また、パフォーマンスはそれほどくないとすぐに私の目標ではありません。これらはすべて楽しみと学習のためです。
私の英語と非常に長い投稿については申し訳ありません。
前もって感謝します。
解決
接線コメント
これらの最初の項目は、あなたが尋ねた質問の接線ですが、...
// Execute
void (*NativeFunction)(char*, char*) = &Plus;
NativeFunction(Params, Result); // Call the function
ここでは「char *」の代わりに「void *」を使用する必要があると思います。関数ポインター型のtypedefもあります:
typedef void (*Operator)(void *params, void *result);
次のように書くことができます:
Operator NativeFunction = Plus;
実際の関数も変更されますが、ごくわずかです:
void Plus(void *pParams, void *pResult)
また、小さな命名問題があります-この関数は、汎用の「任意の2つの型を追加する」関数ではなく、「IntPlusDoubleGivesDouble()」です。
質問への直接的な回答
1)。型のサイズがわかっている場合、プッシュおよびプル機能を変更して構造体のパディングと正確に同期する方法はありますか? (データサイズやエンディアンの問題のようにコンパイラーが処理できるように変更します。)
これを行う簡単な方法はありません。たとえば、次のことを考慮してください。
struct Type1
{
unsigned char byte;
int number;
};
struct Type2
{
unsigned char byte;
double number;
};
一部のアーキテクチャ(たとえば、32ビットまたは64ビットSPARC)では、Type1構造体は4バイト境界で整列された「数値」を持ちますが、Type2構造体は8バイトで整列された「数値」を持ちますバイト境界(16バイト境界に「long double」がある場合があります)。 「個々の要素をプッシュする」戦略は、「バイト」値をプッシュした後にスタックポインターを1バンプします。したがって、スタックポインターがまだ適切でない場合は、「数値」をプッシュする前にスタックポインターを3または7移動します整列。 VMの説明の一部は、特定のタイプに必要なアライメントになります。対応するプッシュコードは、プッシュする前に正しいアライメントを確保する必要があります。
2)。構造を1つずつパックする場合(#pragma pack(1)を使用); (2.1)パフォーマンスのペナルティは許容されますか? (2.2)プログラムの安定性は危険にさらされますか?
x86およびx86_64マシンでは、データを圧縮すると、データアクセスの不整合によりパフォーマンスが低下します。 SPARC やPowerPC などのマシン( mecki による)では、バスエラーが発生するか、代わりに似たようなもの-適切なアライメントでデータにアクセスする必要があります。パフォーマンスを犠牲にして、メモリスペースを節約できます。わずかなスペースでコストをかけながらパフォーマンス(ここでは「クラッシュする代わりに正しく実行する」を含む)を確保する方がよいでしょう。
3)。 2、4、または8のパディングはどうですか?一般的な32ビットまたは64ビットシステムに適しているのはどれですか?
SPARCでは、Nバイトの基本型をNバイトの境界に埋め込む必要があります。 x86では、同じことを行うと最高のパフォーマンスが得られます。
4)。 x86上のGCCについて、正確なパディングアルゴリズムのドキュメントを教えてもらえますか?
マニュアルを読む必要があります。
5)。より良い方法はありますか?
単一の文字の後にタイプが続く 'Type1'トリックは、アライメントの要件を与えることに注意してください-<stddef.h>
:
offsetof(struct Type1, number)
まあ、スタックにデータをパックしません-最高のパフォーマンスを提供するように設定されているため、ネイティブのアライメントで作業します。コンパイラの作成者は、構造にパディングを無造作に追加しません。アーキテクチャに「最適」に機能するため、そこに配置します。よく知っていると決めたら、通常の結果を期待できます-遅いプログラムは時々失敗し、ポータブルではありません。
また、スタックに構造が含まれていると仮定するために、演算子関数でコードを記述することも確信していません。 Params引数を介してスタックから値を引き出し、正しいオフセットとタイプが何であるかを把握します。整数とダブルをプッシュした場合、整数とダブルをプルします(または、逆の順序でダブルと整数をプルします)。珍しいVMを計画していない限り、機能はほとんどありませんsには多くの引数があります。
他のヒント
興味深い投稿で、多くの仕事をしていることを示しています。ほぼ理想的なSO投稿。
準備ができていないので、ご容赦ください。さらに質問をしなければなりません:P
1)。型のサイズがわかっている場合、プッシュおよびプル機能を変更して構造体のパディングと正確に同期する方法はありますか? (データサイズやエンディアンの問題のようにコンパイラーが処理できるように変更します。)
これはパフォーマンスの観点からのみですか?ネイティブの算術型とともにポインターを導入する予定ですか?
2)。構造を1つずつパックする場合(#pragma pack(1)を使用); (2.1)パフォーマンスのペナルティは許容されますか? (2.2)プログラムの安定性は危険にさらされますか?
これは実装定義のものです。プラットフォームを越えて期待できるものではありません。
3)。 2、4、または8のパディングはどうですか?一般的な32ビットまたは64ビットシステムに適しているのはどれですか?
ネイティブの単語サイズと一致する値は、最適なパフォーマンスを提供するはずです。
4)。 x86上のGCCについて言えば、正確なパディングアルゴリズムのドキュメントを教えてもらえますか?
頭のてっぺんを知りません。しかし、 this に似たコードが使用されています。
注: GCCを使用して変数の属性を指定します(パディングをオフにするdefault_struct __attribute__((packed))
と呼ばれるものもあります)。
ここにはいくつかの非常に良い質問があり、それらの多くはいくつかの重要な設計上の問題に絡まりますが、私たちのほとんどにとっては、あなたが取り組んでいるものを見ることができます興味)私たちはあなたの英語が十分に理解できるので、あなたが取り組んでいるのはコンパイラの問題と言語設計の問題です-問題を解決するのは難しくなりますが、すでにJNIで働いているという希望があります...
一つには、プラグマから逃げようとします。多くの人々、非常に多くはそれに反対します。理由の標準的な議論については、問題に関するD言語の立場の正当性を参照してください。別の方法として、16ビットポインターがコードに埋め込まれています。
これらの問題はほぼ無限にあり、十分に研究されており、反対派や壁内の非妥協に埋もれそうです。 Kenneth LoudenのホームページとIntelアーキテクチャマニュアルを読むことをお勧めします。私はそれを持っている、私はそれを読んでみました。データ構造の調整、および議論する他の多くの問題は、歴史的なコンパイラサイエンスに深く埋もれており、誰が何を知っているのかがわかるでしょう。 (予期せぬ結果の俗語または俗語)
それで、ここに行きます:
- Cタイプのサイズ どのようなサイズですか?
- コンピューターエンジニアが移動する前 ソフトウェアエンジニア マイコンを勉強したことがありますか?ハバ、ドンランカスターの作品をご覧ください。
- Pascal、Delphi、そして現在はJavaとPHP プログラマ。 これらはプロセッサの基本的な基本アーキテクチャから比較的削除されていますが、強力で基本的なルーチンを作成するためにどのように使用できるかを多くの人が示したり、示したりします。 David Eckの再帰降下パーサーを調べて、問題の研究を開始する方法を正確に確認することをお勧めします。同様に、Kenneth Loudenには<!> quot; Tiny <!> quot;の実装があります。これは実際のコンパイラです。私はasm dot orgと呼ばれていると思うほど前に何かを見つけました...そこでは非常に高度で強力な研究が利用できましたが、コンパイラサイエンスに入るためにアセンブラで書き始めるのは長い時間を要しました。さらに、ほとんどのアーキテクチャには、プロセッサごとに一貫性のない違いがあります。
- 既存のライブラリへのアクセス
多くのライブラリがありますが、Javaにはいくつか良いライブラリがあります。他の人については知りません。 1つのアプローチは、libを作成することです。 Javaには優れた基盤があり、人々がより良いものを思い付こうとするための余地があります。 Knuth-Morris-Prattまたは何かを改善することから始めます。開始する場所が不足しているだけではありません。 コンピュータープログラミングアルゴリズムディレクトリを試してみてください。確かに、 NISTのアルゴリズムとデータ構造の辞書
- always_inline
必ずしもそうではありません。DovBulkaを参照してください-労働者はCSで博士号を取得しており、時間効率/信頼性-堅牢性などが<!> quot;ビジネスの一部ではない分野で熟練した著者でもありますモデル<!> quot;パラダイムから<!> quot; Oh!関係ない<!> quot;実際に問題になる問題について。
最後に、インストルメンテーションとコントロールは、実際の市場の60%以上を占めています。何らかの理由で、主にビジネスモデルについて耳にします。私はあなたと私が信頼できるソースから持っているtidbitの内側を共有させてください。実際の安全性と財産のリスクは、泥棒、盗難、およびその種のものよりも車両の問題に起因する 10%から60%以上ing。 <!> quot;郡の鉱物採掘施設で90日間のバスティンミネラルの魅力を聞くことはありません!<!> quot;交通チケットの場合、実際、ほとんどの人は交通引用が(N.A.-U.S.A.)クラス4軽罪であり、実際にそのように分類可能であることに気づいていません。
あなたは良い仕事に向けて良い一歩を踏み出したように聞こえます...