在不损失对齐的情况下,最优地打包递归模板化结构体

6
我有一个包含四个字段的结构体,这些字段的类型来自模板参数:
template <typename T1, typename T2, typename T3, typename T4>
struct __attribute__((aligned(8))) four_tuple {
  typedef struct {
    T1 t1;
    T2 t2;
    T3 t3;
    T4 t4;
  } payload;
  payload p;
};

每种类型 T1, T2, T3, 和 T4,都保证是一种基本类型或一个 four_tuple<...>::payload 类型。这些保证是递归的 - 您可以将结构视为编码了一个 四叉树,其叶节点是基本类型。

我的目标是让该结构体具有尽可能小的 sizeof,条件是所有叶节点都得到正确对齐。可用于优化的工具是使用类模板特化:

  • 重新排序字段 t1, t2, t3, t4
  • 添加填充字段
  • payload 上使用 gcc 属性 packed
  • 可能还有其他方法?

我感觉使用 enable_if 和 SFINAE 可以找到这个问题的巧妙解决方案。有人能找到吗?

为了说明问题,如果我们直接使用上述实现 using Foo = four_tuple<char,double,char,double>,我们的有效负载和总大小将为32。如果我们简单地声明有效负载 packed,那么 double 将不会很好地对齐。一个模板特化,以递减顺序重新排序字段(这里是 double, double, char, char),将给出24的有效负载和总大小。但它使用的额外6个字节是浪费的,可以通过考虑 using Bar = four_tuple<Foo::payload,int,int,int> 来看到这一点。在最佳包装下,Bar 可以适合32字节,但是使用此方案需要40字节。直接应用字段重新排序与 packed 将导致 Bar 中的 int 不对齐 - 需要一些填充。
我知道通常重构结构体字段的内存布局可能会由于缓存考虑而产生性能影响,并且通常这些影响至少与更好的打包潜在收益同样重要。尽管如此,我仍想探索权衡,但在我的情况下,如果不解决这个问题,我无法正确地进行。

我没有看到它。如果您重新描述的方式,但不添加“packed”,会有什么问题吗?在另一种类型中使用它不会产生任何未对齐的字段,因为编译器已经通过填充来修复了这个问题。 - user743382
顺便问一下,为什么你要在结构体上强制对齐?如果你的类型是四个 char,那么你想要的大小就是 4,不是吗?这不需要一个 8 的对齐。 - user743382
如果正确打包,@hvd four_tuple<Foo::payload,int,int,int> 可以达到32的大小且不会出现错误对齐。如果您不添加packed,它将具有40的大小。 - dshin
解决我的问题只能保证相对于“four_tuple”,叶子节点将对齐。如果特定的“four_tuple”实例未对齐,则所有赌注都会失效。这就是为什么我要对齐8的原因。 - dshin
https://dev59.com/CHfZa4cB1Zd3GeqPOjEx 有关编译时重新排列以优化打包的详细讨论。该答案还提到了 https://rmf.io/cxx11/optimal-tuple-i/,其中讨论了元组数据布局。 - Jens
显示剩余4条评论
1个回答

1
你嵌套的元组中存在一个大问题,就是你想要一个类型为four_tuple<char,double,char,double>::payload的字段,它的对齐方式与four_tuple<char,double,char,double>相同,但不要求容器类型继承其对齐方式。这很复杂。虽然你在问题中已经建议使用GCC扩展,但这样做会使你的代码高度不可移植到除GCC以外的任何地方。基本思路是可以使用位域来插入填充以确保对齐:
struct __attribute__((packed)) S {
  char c; // at offset 0
  int i; // at offset 1, not aligned
  int : 0;
  int j; // at offset 8, aligned
  int : 0;
  int k; // at offset 12, no extra padding between j and k
};

int是一种非常具体的类型,具有非常特定的对齐方式,而您需要动态确定对齐方式。幸运的是,GCC允许使用类型为char的位域,这通常只强制字节对齐,与alignas组合使用,确保任意对齐。

完成后,您可以检查所有24种可能的字段顺序,并选择总大小最小的有效负载。我将有效负载设置为全局类型,并增加了一个额外的模板参数以指示字段顺序。这使得tuple4<T1,T2,T3,T4>可以依次检查tuple4_payload<T1,T2,T3,T4,1234>tuple4_payload<T1,T2,T3,T4,1243>等,并选择最佳选项。

template <typename...> struct smallest;
template <typename...T> using smallest_t = typename smallest<T...>::type;

template <typename T> struct smallest<T> { using type = T; };
template <typename T, typename...Ts> struct smallest<T, Ts...> { using type = std::conditional_t<sizeof(T) <= sizeof(smallest_t<Ts...>), T, smallest_t<Ts...>>; };

template <typename T1, typename T2, typename T3, typename T4> struct tuple4;
template <typename T1, typename T2, typename T3, typename T4, int fieldOrder> struct tuple4_payload;
template <typename T1, typename T2, typename T3, typename T4> struct tuple4_simple { T1 t1; T2 t2; T3 t3; T4 t4; };

template <typename T> struct extract_payload { using type = T; };
template <typename...T> struct extract_payload<tuple4<T...>> { using type = typename tuple4<T...>::payload; };
template <typename T> using extract_payload_t = typename extract_payload<T>::type;

#define PERMS \
  PERM(1,2,3,4) PERM(1,2,4,3) PERM(1,3,2,4) PERM(1,3,4,2) PERM(1,4,2,3) PERM(1,4,3,2) \
  PERM(2,1,3,4) PERM(2,1,4,3) PERM(2,3,1,4) PERM(2,3,4,1) PERM(2,4,1,3) PERM(2,4,3,1) \
  PERM(3,1,2,4) PERM(3,1,4,2) PERM(3,2,1,4) PERM(3,2,4,1) PERM(3,4,1,2) PERM(3,4,2,1) \
  PERM(4,1,2,3) PERM(4,1,3,2) PERM(4,2,1,3) PERM(4,2,3,1) PERM(4,3,1,2) PERM(4,3,2,1)

#define PERM(a,b,c,d) \
  template <typename T1, typename T2, typename T3, typename T4> \
  struct __attribute__((packed)) tuple4_payload<T1, T2, T3, T4, a##b##c##d> { \
    char : 0 alignas(T##a); extract_payload_t<T##a> t##a; \
    char : 0 alignas(T##b); extract_payload_t<T##b> t##b; \
    char : 0 alignas(T##c); extract_payload_t<T##c> t##c; \
    char : 0 alignas(T##d); extract_payload_t<T##d> t##d; \
  };
PERMS
#undef PERM

#define PERM(a,b,c,d) , tuple4_payload<T1, T2, T3, T4, a##b##c##d>
template <typename, typename...T> using tuple4_smallest_payload_t = smallest_t<T...>;
template <typename T1, typename T2, typename T3, typename T4>
struct alignas(tuple4_simple<T1, T2, T3, T4>) tuple4 : tuple4_smallest_payload_t<void PERMS> {
  using payload = tuple4_smallest_payload_t<void PERMS>;
};
#undef PERM

在您的情况下,您将使用此代码:tuple4<int,tuple4<char,double,char,double>,int,int>。请注意,即使未明确提及有效载荷类型,它仍将用于t2成员。

感谢您的工作。这段代码在我的电脑上无法编译,但我认为我理解了它的思路。如果我理解正确的话,它仍然不会是最优的,因为 tuple4<tuple4<8,4,4,2>, tuple4<2,4,8,8>, 8, 8> 将使用填充空间,而一个天真的打包实现没有重新排序将不使用填充并满足原始类型不对齐的要求。(这里,2=int16_t4=int32_t8=int64_t)。但它应该比我拥有的任何其他东西都要好用。 - dshin
@dshin 它可以使用GCC 4.9及更高版本编译,如果您将conditional_t<...>更改为conditional<...>::type,则可以使用4.8。是的,你是对的,在你的例子中它不会是最优的。由于packed只能删除尾部填充,如果您想要保持对象开头的对齐,tuple4<2,4,8,8>会被重新排序为tuple4<8,8,4,2>,此时您首选的布局就无法实现了。我看不到任何解决办法,抱歉。 - user743382
我可以想象理论上如果 tuple4::payload 类型有8个静态布尔值,对应着它是否可以被{0, 1, 2, 3, 4, 5, 6, 7}字节偏移,那么元算法可以将其纳入详尽搜索。但是需要一些巧妙的方法来缩小搜索空间。 - dshin
基本上,您最终会得到一个对齐要求为8*N+2的对齐要求,这很棘手,并且不会与任何编译器内置的对齐功能良好地配合使用。但可能存在问题的另一点是,不能确定元组的理想布局而不考虑其包含的元组(如果有):给定 tuple4<tuple4<2,8,8,8>,tuple4<2,8,8,8>,8,8> :您最好将其排序为 8,8,8,2,2,8,8,8,8,8,但这意味着第一个 tuple4<2,8,8,8> 和第二个 tuple4<2,8,8,8> 将不具有相同的布局。 - user743382
你说得对,无论上下文如何,负载布局必须是固定的。因此,这似乎是最好的解决方案。 - dshin

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接