【问题标题】:Implement a struct type on top of void *在 void * 之上实现一个结构类型
【发布时间】:2016-11-27 02:25:55
【问题描述】:

这里是简单的健全性检查问题。基本要求是将两个灵活的数组成员放在一个结构中,以减少对 malloc 的调用次数,以提高性能。

鉴于结构实例是一个对齐的内存块,其中包含多个恒定偏移量的字段,是否可以通过编写偏移计算和强制转换来实现在语义上与结构等效的功能?

void f()
{
  typedef struct
  {
    double x;
    char y;
    int32_t foo;
    double z;
  } equivalent;
  equivalent * e = malloc(sizeof(equivalent));
  free(e);

  static_assert(sizeof(equivalent) == 24,"");
  char* memory = malloc(24);
  double* x    = (double*)  ( 0 + memory);
  char* y      = (char *)   ( 8 + memory);
  int32_t* foo = (int32_t*) (12 + memory);
  double* z    = (double*)  (16 + memory);
  free(memory);
}

保持对齐/偏移计算一致是乏味的,但假设类型是不透明的,客户端代码不必看到任何这些。同样,语法开销也被隐藏了。

我已通读 C11(“有效类型”部分)阐明的别名规则,并认为我在那里很清楚。

这是公平的游戏吗?在编写大量非常枯燥的代码之前,我想我会寻求第二个意见。

干杯

编辑:作为对 Jonathan Leffler 的回应,这是我打算如何将几个运行时确定长度的数组放入单个内存块的快速而粗略的草图。

我更喜欢存储一个用于计算数组位置的整数,而不是存储一个已经指向数组的指针,因为它使复制结构更简单。不过,存储适当初始化的指针并在副本上重新定位它们可能会更快。

void* g(uint64_t N_first, uint64_t N_second)
{
  // desired representation:                                                                                                                                                     
  // uint64_t N_first;                                                                                                                                                           
  // int32_t first[N_first];                                                                                                                                                     
  // uint64_t N_second;                                                                                                                                                          
  // double second[N_second];                                                                                                                                                    
  // this function doesn't populate the arrays, only                                                                                                                             
  // allocates storage and sets up the length fields                                                                                                                             

  uint64_t bytes_for_lengths = 16;

  char* bytes = malloc(bytes_for_lengths + bytes_for_first(N_first) +
                       bytes_for_second(N_second));

  uint64_t* ptr_N_first = get_N_first(bytes);
  *ptr_N_first = N_first;

  uint64_t* ptr_N_second = get_N_second(bytes);
  *ptr_N_second = N_second;

  return (void*)bytes;
}

// I haven't decided how best to factor out the field access
// and associated functions yet, so this is not optimal

uint64_t* get_N_first(void* vdata)
{
  char* data = (char*)vdata;
  return (uint64_t*)(data + 0);
}
int32_t* get_first(void* vdata)
{
  char * data = (char*)vdata;
  return (int32_t*)(data + 8);
}
uint64_t bytes_for_first(uint64_t N_first)
{
  // first is an int32_t                                                                                                                                                         
  // the next field needs to be 8 byte aligned                                                                                                                                   
  uint64_t bytes = 4 * N_first;
  if (bytes % 8 != 0)
    {
      bytes += 4;
    }
  return bytes;
}

uint64_t* get_N_second(void* vdata)
{
  uint64_t n_first = *get_N_first(vdata);
  uint64_t first_bytes = bytes_for_first(n_first);
  char* data = (char*)vdata;
  return (uint64_t*)(data + 8 + first_bytes);
}
double* get_second(void* vdata)
{
  char * data = (char*)vdata;
  uint64_t n_first = *get_N_first(vdata);
  uint64_t first_bytes = bytes_for_first(n_first);
  return (double*)(data + 8 + first_bytes + 8);
}
uint64_t bytes_for_second(uint64_t N_second)
{
  // second is a double                                                                                                                                                          
  return 8 * N_second;
}

【问题讨论】:

  • 你在这里做的很好。尚不清楚您将如何调整它以实现“结构中的两个灵活数组成员”。你不能直接这样做;您可以通过在结构中使用两个指针和连续分配的内存来小心地获得近似结果,但您必须小心。
  • @JonathanLeffler 谢谢。我已经添加了我打算如何适应它的草图。这显然很容易出错,但如果 C 愿意玩得好,这是可以解决的。
  • 代码编译后这到底有什么好处?我会假设任何一半体面的编译器都会生成或多或少相同的代码,或者至少与访问 steuct 成员或进行手动偏移的代码一样快。但我可能只是看不到这里的重点
  • uint64_t bytes_for_lengths = 16; 2 * sizeof(uint64_t) 等。
  • 作为一般规则,您最好避免在界面中使用void *。您可以将任何指针类型传递给接受void * 的函数;你没有类型检查。您最好使用不透明的结构类型(可以使用struct Opaque; 声明)并将该类型传递给您的函数。然后尝试传递struct tm *int *char * 将导致编译警告。或者使用完全定义的结构类型。

标签: c types casting c11


【解决方案1】:

没有什么乏味的......

size_t offset_of_x = offsetof(equivalent, x);
size_t offset_of_y = offsetof(equivalent, y);
size_t offset_of_foo = offsetof(equivalent, foo);
size_t offset_of_z = offsetof(equivalent, z);

char* memory = malloc(sizeof(equivalent));
double* x    = offset_of_x   + memory;
char* y      = offset_of_y   + memory;
int32_t* foo = offset_of_foo + memory;
double* z    = offset_of_z   + memory;
free(memory);

是的,这是完全合法的。

/edit(编辑后):

而不是使用这种表示:

struct fake_your_version {
    uint64_t N_first;
    int32_t first[N_first];
    uint64_t N_second;
    double second[N_second];
};

您应该考虑使用以下任一表示:

struct fake_alternative_1 {
    uint64_t size; // max over all num[i]
    uint64_t num[2]; // num[0] being for first, num[1] being for second
    struct {
        int32_t first;
        double second;
    } entry[num];
};

或者这个表示:

struct fake_alternative_2 {
    uint64_t num[2];
    void * data[2]; // separate malloc(num[i] * sizeof(whatever));
};

因为您的方法将强制在除最后一个数组之外的任何大小变化时移动数据。

fake_alternative_1 还将保存一个 malloc(如果数组需要不同大小,则以填充字节和丢失内存为代价)。

在你考虑这样做之前,你真的应该问问自己,malloc 真的那么慢,你必须避免它。很可能,无论您在做什么,malloc 之外的其他东西都会拖慢您的速度(可能,您尝试保存 mallocs 会使您的代码变得更慢而不是更快)。

fake_alternative_2 会接受,每个数组都有它自己的malloc,但我想,我给你这个替代方案并没有告诉你任何新的东西)。

【讨论】:

  • 哦,我不知道它是官方的,但实际上它是从 C89 开始的......
【解决方案2】:

我不禁觉得使用简单的结构来实现您的“双 VLA”结构类型会更简洁。或多或少是这样的:

// desired representation:
// uint64_t N_first;
// int32_t first[N_first];
// uint64_t N_second;
// double second[N_second];

#include <assert.h>
#include <inttypes.h>
#include <stdalign.h>
#include <stdio.h>
#include <stdlib.h>

struct DoubleVLA
{
    uint64_t  N_first;
    int32_t  *first;
    uint64_t  N_second;
    double   *second;
    //double    align_32[];     // Ensures alignment on 32-bit
};

extern struct DoubleVLA *alloc_DoubleVLA(uint64_t n1, uint64_t n2);

struct DoubleVLA *alloc_DoubleVLA(uint64_t n1, uint64_t n2)
{
    struct DoubleVLA *dv = malloc(sizeof(*dv) + n1 * sizeof(dv->first) + n2 * sizeof(dv->second));
    if (dv != 0)
    {
        dv->N_first = n1;
        dv->N_second = n2;
        if (alignof(dv->second) >= alignof(dv->first))
        {
            dv->second = (double *)((char *)dv + sizeof(*dv));
            dv->first  = (int32_t *)((char *)dv + sizeof(*dv) + n2 * sizeof(dv->second));
        }
        else
        {
            dv->first  = (int32_t *)((char *)dv + sizeof(*dv));
            dv->second = (double *)((char *)dv + sizeof(*dv) + n1 * sizeof(dv->first));
        }
    }
    return dv;
}

int main(void)
{
    struct DoubleVLA *dv = alloc_DoubleVLA(UINT64_C(11), UINT64_C(32));
    for (uint64_t i = 0; i < dv->N_first; i++)
        dv->first[i] = i * 100 + rand() % 100;
    for (uint64_t j = 0; j < dv->N_second; j++)
        dv->second[j] = j * 1000.0 + (rand() % 100000) / 100.0;
    for (uint64_t i = 0; i < dv->N_first; i++)
        printf("%.2" PRIu64 " = %12" PRId32 "\n", i, dv->first[i]);
    for (uint64_t j = 0; j < dv->N_second; j++)
        printf("%.2" PRIu64 " = %12.2f\n", j, dv->second[j]);
    free(dv);
    return 0;
}

即使在 32 位平台上,结构的末尾也应该有足够的填充以使其大小合适,以便在结构之后立即为 double 数组和 @987654323 数组适当对齐@ 之后。但是,通过在结构中将两个大小放在首位,两个指针放在最后,可以避免不必要的填充。这在 64 位平台上不是问题。可选的align_32 VLA假设int32_t的对齐要求不大于double的对齐要求;即使有一些奇怪的对齐限制或要求,它也会确保正确填充结构。可以提供满足约束的静态断言。

alignof 材质来自 C11;它允许您使用具有不同对齐要求的两种类型,并自动选择更好的布局(在不太严格的一个之前对齐更严格的数组)。

有了这种组织结构,就不需要为结构的各个部分提供功能接口。直接访问简单易懂。

【讨论】:

  • 我喜欢这个,谢谢你写出来。我不确定在指针中缓存地址查找是否值得,但是将固定字段放在结构中然后为数组分配结构+足够的空间会减少很多样板。不错。
猜你喜欢
  • 2016-10-08
  • 2020-11-25
  • 1970-01-01
  • 1970-01-01
  • 2011-07-20
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-02-23
相关资源
最近更新 更多