跨平台VM的C内存管理
-
06-07-2019 - |
题
我问了一个关于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;
}
本机函数的推送,拉取和调用可以通过字节代码(即稍后将如何制作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,那将是非常好的。但至少有三个与记忆相关的问题我们必须要注意。
1)。数据大小 - 看起来如果我在同一架构上使用相同的编译器编译VM和本机函数,则大小类型应该相同。
2)。字节顺序 - 与数据大小相同。
3)。内存对齐 - 问题在于,可以在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
我认为你应该在这里使用'void *'而不是'char *'。我还有一个函数指针类型的typedef:
typedef void (*Operator)(void *params, void *result);
然后你可以写:
Operator NativeFunction = Plus;
实际的功能也会被修改 - 但只是非常轻微:
void Plus(void *pParams, void *pResult)
另外,你有一个小的命名问题 - 这个函数是'IntPlusDoubleGivesDouble()',而不是通用'添加任何两种类型'的函数。
直接回答问题
1)。如果我知道类型的大小,有没有办法修改推拉函数以与struct padding完全同步? (修改让编译器像Datasize和Endians一样处理它)。
没有一种简单的方法可以做到这一点。例如,考虑:
struct Type1
{
unsigned char byte;
int number;
};
struct Type2
{
unsigned char byte;
double number;
};
在某些体系结构(例如32位或64位SPARC)上,Type1结构将在4字节边界处对齐“number”,但Type2结构将在8-上对齐“number”字节边界(可能在16字节边界上有一个'long double')。在推送'byte'值之后,你的'推送单个元素'策略会将堆栈指针碰到1 - 所以如果堆栈指针尚未正确,你可能希望在推送'number'之前将堆栈指针移动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'技巧可以提供对齐要求 - 可能使用<stddef.h>
中的'offsetof()'宏:
offsetof(struct Type1, number)
好吧,我不会将数据打包到堆栈中 - 我会使用本机对齐,因为它设置为提供最佳性能。编译器编写器不会空闲地向结构添加填充;他们把它放在那里因为它对建筑来说“最好”。如果您决定自己了解得更好,那么您可能会遇到常见的后果 - 有时会失败并且不那么便携的慢速程序。
我也不相信我会在运算符函数中编写代码来假设堆栈包含一个结构。我会通过Params参数从堆栈中提取值,知道正确的偏移和类型是什么。如果我按下一个整数和一个double,那么我会拉一个整数和一个double(或者,可能是相反的顺序 - 我会拉一个double和一个int)。除非您计划一个不寻常的VM,否则很少有功能s会有很多争论。
其他提示
有趣的帖子,表明你投入了大量的工作。几乎是理想的SO帖子。
我没有现成的答案,所以请耐心等待。我将不得不再问几个问题:P
1)。如果我知道类型的大小,有没有办法修改推拉函数以与struct padding完全同步? (修改让编译器像Datasize和Endians一样处理它)。
这只是从性能的角度来看吗?您是否计划引入指针以及本机算术类型?
2)。如果我将结构打包一个(使用#pragma pack(1)); (2.1)性能损失是否可以接受? (2.2)程序稳定性是否会有风险?
这是一个实现定义的东西。不是你可以指望跨平台的东西。
3)。填充2,4或8怎么样?对于一般的32位或64位系统哪个应该好?
与本机字大小匹配的值应该能够为您提供最佳性能。
4)。你能指导我一个精确的填充算法的文档,比如x86上的GCC吗?
我不知道任何一个问题。但我看到的代码类似于这个。
请注意,您可以使用GCC指定变量的属性(也有一些叫做default_struct __attribute__((packed))
的关闭填充的东西)。
这里有一些非常好的问题,其中很多都会在一些重要的设计问题上纠结,但对于我们大多数人来说 - 我们可以看到你正在努力的方向(在我写作的时候只是张贴,所以你可以看到你正在生成我们可以很好地理解你的英语,你正在努力解决的是一些编译器问题和一些语言设计问题 - 这个问题变得很困难,但你已经在JNI工作了,希望......
首先,我会试图摆脱pragma;许多人很多会不同意这一点。关于为什么在这个问题上看到D语言立场的理由的规范讨论。另一方面,代码中隐藏了一个16位指针。
这些问题几乎无穷无尽,研究得很好,很可能会让我们陷入反对和内心的不妥协之中。如果我可以建议阅读 Kenneth Louden的主页以及英特尔架构手册。我有它,我试着读它。数据结构一致性以及您提出的许多其他问题都深深植根于历史编译器科学中,很可能会让您充分了解谁知道什么。 (对于不可预见的后果而言,俚语或惯用法)
说完了,这就是:
- C型尺寸 什么类型的尺寸?
- 计算机工程师搬到之前 软件工程师 曾经研究过微控制器? Hava看看Don Lancaster的一些作品。
- Pascal,Delphi,现在是Java和PHP 程序员。强> 这些与处理器的基本基础架构相比已经相对较少,尽管很多人会展示或试图展示它们如何用于编写强大而基本的例程。我建议看David Eck的递归下降解析器,看看究竟如何开始研究这个问题。同样,Kenneth Louden实现了<!>“Tiny <!>”;这是一个实际的编译器。我不久前发现了一些东西,我认为它被称为asm dot org ...非常先进,非常强大的工作可用于那里的研究,但是开始编写汇编程序以进入编译器科学是一件很长的事情。此外,大多数体系结构的差异从一个处理器到另一个处理器不一致。
- 访问现有资料库 醇>
- always_inline 醇>
有很多库,Java有一些好的库。我不知道其他人。一种方法是尝试编写lib。 Java有一个很好的基础,为人们想要尝试提出更好的东西留出空间。首先要改进Knuth-Morris-Pratt或其他东西:开始的地方并不缺乏。尝试计算机编程算法目录,请务必查看算法和数据结构词典
不一定,请参阅Dov Bulka--工作人员持有CS博士学位,并且在时间效率/可靠性稳健等方面不受某些<!>“业务影响的领域也是一位熟练的作者<!>模型QUOT;范式从哪里得到一些<!>“哦!无关紧要<!>关于实际上很重要的问题。
作为结束语,如您所述,仪器和控制占完成编程技能的60%以上。出于某种原因,我们主要听说的是商业模式。让我与你分享我和来自可靠消息来源的内部消息。从 10%到60%或更多实际的安全和财产风险来自车辆问题,而不是来自窃贼,盗窃和那种类型ING。在县矿产开采设施中,你永远不会听到<!> 90天bustin矿物的呼吁!<!>对于交通票,实际上大多数人甚至没有意识到交通引用(N.A.-美国)第4类轻罪并且实际上是可归类的。
听起来像你已朝着一些好的工作迈出了一大步,......