VLA 原型和多维数组参数
VLA prototype and multidimensional array argument
我创建了一个 C99 VLA 函数:
void create_polygon(int n, int faces[][n]);
我想在另一个分配我的二维数组的函数中调用这个函数:
void parse_faces()
{
int faces[3][6];
create_polygon(6, faces);
}
当我传递一个二维数组作为参数时,它传递一个指向 6 整数数组的指针,引用调用函数中的堆栈内存。
此处的 VLA 参数仅用作类型声明(不分配任何实际内存),告诉编译器使用 ((int*)faces)[i * 6 + j]
而不是 faces[i][j]
以行优先顺序访问数据。
使用 VLA 参数或固定大小声明函数有什么区别?
faces[i][j]
总是 等同于 *(*(faces + i) + j)
,无论是否有 VLA。
现在让我们比较两个变体(不考虑您实际上还需要外部维度来防止迭代时超出数组边界):
void create_polygon1(int faces[][6]);
void create_polygon2(int n, int faces[][n]);
传递给最初的数组是作为经典数组还是作为 VLA 创建的并不重要,第一个函数接受长度恰好为 6 的数组,第二个函数可以接受任意长度的数组(假设到目前为止已经清楚... ).
faces[i][j]
现在将翻译为:
*((int*)faces + (i * 6 + j)) // (1)
*((int*)faces + (i * n + j)) // (2)
差异看起来微乎其微,但在汇编程序级别上可能会变得更加明显(假设所有变量都存储在堆栈中;假设 sizeof(int) == 4
):
LD R1, i;
LD R2, j;
MUL R1, R1, 24; // using a constant! 24: 6 * sizeof(int)!
MUL R2, R2, 4; // sizeof(int)
ADD R1, R2, R2; // index stored in R1 register
LD R1, i;
LD R2, j;
LD R3, m; // need to load from stack
MUL R3, R3, 4; // need to multiply with sizeof(int) yet
MUL R1, R1, R3; // can now use m from register R3
MUL R2, R2, 4; // ...
ADD R1, R2, R2; // ...
当然,真正的汇编代码可能会有所不同,特别是如果您使用允许在寄存器中传递一些参数的调用约定(那么可能不需要将 n 加载到 R3 中)。
为了完整性(由于评论而添加,与原始问题无关):
还有 int* array[]
案例:由指向数组的指针数组表示。
*((int*)faces + (i * ??? + j))
不再起作用,因为 faces
在这种情况下不是连续的内存(当然,指针本身在连续的内存中,但不是所有 faces[i][j]
)。我们被迫做:
*(*(faces + i) + j)
因为我们需要在应用下一个索引之前取消引用数组中的真实指针。汇编代码(为了比较,首先需要指向二维数组的指针的更完整变体):
LD R1, faces;
LD R2, i;
LD R3, j;
LD R4, m; // or skip, if no VLA
MUL R4, R4, 4; // or skip, if no VLA
MUL R2, R2, R3; // constant instead of R3, if no VLA
MUL R3, R3, 4;
ADD R2, R2, R3; // index stored in R1 register
ADD R1, R1, R2; // offset from base pointer
LD R1, [R1]; // loading value of faces[i][j] into register
LD R1, faces;
LD R2, i;
LD R3, j;
MUL R2, R2, 8; // sizeof(void*) (any pointer)
MUL R3, R3, 4; // sizeof(int)
ADD R1, R1, R2; // address of faces[i]
LD R1, [R1]; // now need to load address - i. e. de-referencing faces[i]
ADD R1, R1, R3; // offset within array
LD R1, [R1]; // loading value of faces[i][j] into register
我反汇编了这段代码:
void create_polygon(int n, int faces[][6])
{
int a = sizeof(faces[0]);
(void)a;
}
使用 VLA 参数:
movl %edi, -4(%rbp) # 6
movq %rsi, -16(%rbp) # faces
movl %edi, %esi
shlq , %rsi # 6 << 2 = 24
movl %esi, %edi
固定尺寸:
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl , %edi # 24
正如 Aconcagua 指出的那样,在使用 VLA 的第一个示例中,通过将 int
的大小乘以第二维的大小,在 运行 时间计算大小,即存储在 rsi
中的参数,然后移动到 edi
.
在第二个示例中,大小是在编译时直接计算并放入edi
。主要优点是能够在传递不同大小时检查不正确的指针类型参数,从而避免崩溃。
我创建了一个 C99 VLA 函数:
void create_polygon(int n, int faces[][n]);
我想在另一个分配我的二维数组的函数中调用这个函数:
void parse_faces()
{
int faces[3][6];
create_polygon(6, faces);
}
当我传递一个二维数组作为参数时,它传递一个指向 6 整数数组的指针,引用调用函数中的堆栈内存。
此处的 VLA 参数仅用作类型声明(不分配任何实际内存),告诉编译器使用 ((int*)faces)[i * 6 + j]
而不是 faces[i][j]
以行优先顺序访问数据。
使用 VLA 参数或固定大小声明函数有什么区别?
faces[i][j]
总是 等同于 *(*(faces + i) + j)
,无论是否有 VLA。
现在让我们比较两个变体(不考虑您实际上还需要外部维度来防止迭代时超出数组边界):
void create_polygon1(int faces[][6]);
void create_polygon2(int n, int faces[][n]);
传递给最初的数组是作为经典数组还是作为 VLA 创建的并不重要,第一个函数接受长度恰好为 6 的数组,第二个函数可以接受任意长度的数组(假设到目前为止已经清楚... ).
faces[i][j]
现在将翻译为:
*((int*)faces + (i * 6 + j)) // (1)
*((int*)faces + (i * n + j)) // (2)
差异看起来微乎其微,但在汇编程序级别上可能会变得更加明显(假设所有变量都存储在堆栈中;假设 sizeof(int) == 4
):
LD R1, i;
LD R2, j;
MUL R1, R1, 24; // using a constant! 24: 6 * sizeof(int)!
MUL R2, R2, 4; // sizeof(int)
ADD R1, R2, R2; // index stored in R1 register
LD R1, i;
LD R2, j;
LD R3, m; // need to load from stack
MUL R3, R3, 4; // need to multiply with sizeof(int) yet
MUL R1, R1, R3; // can now use m from register R3
MUL R2, R2, 4; // ...
ADD R1, R2, R2; // ...
当然,真正的汇编代码可能会有所不同,特别是如果您使用允许在寄存器中传递一些参数的调用约定(那么可能不需要将 n 加载到 R3 中)。
为了完整性(由于评论而添加,与原始问题无关):
还有 int* array[]
案例:由指向数组的指针数组表示。
*((int*)faces + (i * ??? + j))
不再起作用,因为 faces
在这种情况下不是连续的内存(当然,指针本身在连续的内存中,但不是所有 faces[i][j]
)。我们被迫做:
*(*(faces + i) + j)
因为我们需要在应用下一个索引之前取消引用数组中的真实指针。汇编代码(为了比较,首先需要指向二维数组的指针的更完整变体):
LD R1, faces;
LD R2, i;
LD R3, j;
LD R4, m; // or skip, if no VLA
MUL R4, R4, 4; // or skip, if no VLA
MUL R2, R2, R3; // constant instead of R3, if no VLA
MUL R3, R3, 4;
ADD R2, R2, R3; // index stored in R1 register
ADD R1, R1, R2; // offset from base pointer
LD R1, [R1]; // loading value of faces[i][j] into register
LD R1, faces;
LD R2, i;
LD R3, j;
MUL R2, R2, 8; // sizeof(void*) (any pointer)
MUL R3, R3, 4; // sizeof(int)
ADD R1, R1, R2; // address of faces[i]
LD R1, [R1]; // now need to load address - i. e. de-referencing faces[i]
ADD R1, R1, R3; // offset within array
LD R1, [R1]; // loading value of faces[i][j] into register
我反汇编了这段代码:
void create_polygon(int n, int faces[][6])
{
int a = sizeof(faces[0]);
(void)a;
}
使用 VLA 参数:
movl %edi, -4(%rbp) # 6
movq %rsi, -16(%rbp) # faces
movl %edi, %esi
shlq , %rsi # 6 << 2 = 24
movl %esi, %edi
固定尺寸:
movl %edi, -4(%rbp)
movq %rsi, -16(%rbp)
movl , %edi # 24
正如 Aconcagua 指出的那样,在使用 VLA 的第一个示例中,通过将 int
的大小乘以第二维的大小,在 运行 时间计算大小,即存储在 rsi
中的参数,然后移动到 edi
.
在第二个示例中,大小是在编译时直接计算并放入edi
。主要优点是能够在传递不同大小时检查不正确的指针类型参数,从而避免崩溃。