Naw*_*Man 6 c memory-management cross-platform low-level vm-implementation
我问一个问题,关于C型尺寸,我得到一个很好的答案,但我意识到,我可能不会制定这个问题非常好是我的目的.
我的背景来自计算机工程师,然后转到软件工程师,所以我喜欢计算机架构,并且总是考虑制作VM.我刚刚完成了一个有趣的Java项目,我很自豪.但是有些法律问题我现在无法开源,而且我现在有空闲时间.所以我想看看我是否可以在C上制作另一个VM(速度更快),只是为了娱乐和教育.
事情是,我上次写一篇非琐事C问题时,我不是一个C程序,这是10多年前的事了.我是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;
}
Run Code Online (Sandbox Code Playgroud)
本机函数的推送,拉取和调用可以由字节代码(即稍后将如何创建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));
}
Run Code Online (Sandbox Code Playgroud)
在本机功能方面,我为测试目的创建了以下内容:
// 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
Run Code Online (Sandbox Code Playgroud)
这在我的机器上运行良好(Linux x86 32位GCC-C99).如果这也适用于其他OS/Architecture,那将是非常好的.但至少有三个与记忆相关的问题我们必须要注意.
1).数据大小 - 似乎如果我在同一架构上使用相同的编译器编译VM和本机函数,则大小类型应该相同.
2).字节顺序 - 与数据大小相同.
3).内存对齐 - 这是问题,因为padding-bytes可能会在struct中添加,但是在准备参数堆栈时很难同步它(除了硬编码之外,无法知道如何添加填充).
我的问题是:
1).如果我知道类型的大小,有没有办法修改推拉函数以与struct padding完全同步?(修改让编译器像Datasize和Endians问题一样处理它).
2).如果我把结构打包(使用#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
Run Code Online (Sandbox Code Playgroud)
我认为你应该在这里使用“void *”而不是“char *”。我还有一个函数指针类型的 typedef:
typedef void (*Operator)(void *params, void *result);
Run Code Online (Sandbox Code Playgroud)
然后你可以写:
Operator NativeFunction = Plus;
Run Code Online (Sandbox Code Playgroud)
实际的功能也会被修改 - 但只有很小的修改:
void Plus(void *pParams, void *pResult)
Run Code Online (Sandbox Code Playgroud)
另外,您还有一个小命名问题 - 该函数是“IntPlusDoubleGivesDouble()”,而不是通用的“添加任意两种类型”函数。
1)。如果我知道类型的大小,有没有办法修改推和拉函数以与结构填充完全同步?(修改为让编译器像 Datasize 和 Endians 问题一样处理它)。
没有一个简单的方法可以做到这一点。例如,考虑:
struct Type1
{
unsigned char byte;
int number;
};
struct Type2
{
unsigned char byte;
double number;
};
Run Code Online (Sandbox Code Playgroud)
在某些体系结构(例如 32 位或 64 位 SPARC)上,Type1 结构将在 4 字节边界上对齐“数字”,但 Type2 结构将在 8 字节边界上对齐“数字”(并且可能在 16 字节边界上有一个“long double”)。您的“推送单个元素”策略会在推送“字节”值后将堆栈指针增加 1 - 因此,如果堆栈指针尚未正确,您可能希望在推送“数字”之前将堆栈指针移动 3 或 7对齐。VM 描述的一部分将是任何给定类型所需的对齐;相应的推送代码在推送之前需要确保正确对齐。
2)。如果我将结构打包一(使用#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”技巧为您提供了对齐要求 - 可能使用以下中的“offsetof()”宏<stddef.h>
:
offsetof(struct Type1, number)
Run Code Online (Sandbox Code Playgroud)
好吧,我不会将数据打包在堆栈上 - 我会使用本机对齐,因为这设置为提供最佳性能。编译器编写者不会随意向结构添加填充;他们把它放在那里是因为它对架构来说“最好”。如果您认为自己了解得更多,则可以预见通常的后果 - 速度较慢的程序有时会失败并且不那么可移植。
我也不相信我会在运算符函数中编写代码来假设堆栈包含结构。我会通过 Params 参数从堆栈中取出值,知道正确的偏移量和类型是什么。如果我推入一个整数和一个双精度数,那么我会拉出一个整数和一个双精度数(或者,也许以相反的顺序 - 我会拉出一个双精度数和一个整数)。除非您正在计划一个不寻常的虚拟机,否则很少有函数会有很多参数。