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。主要优点是能够在传递不同大小时检查不正确的指针类型参数,从而避免崩溃。