【问题标题】:Optimally packing a recursively templated struct without loss of alignment在不丢失对齐的情况下优化打包递归模板结构
【发布时间】:2016-01-08 07:28:46
【问题描述】:

我有一个来自模板参数的 4 个类型字段的结构:

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;
};

T1T2T3T4 的每个类型都保证是原始类型或 four_tuple&lt;...&gt;::payload 类型。保证是递归的 - 您可以将结构视为对叶节点为原始类型的 quadtree 进行编码。

我的目标是使结构具有最小可能的sizeof,条件是所有叶节点都正确对齐。允许优化的工具是类模板特化,使用:

  • 重新排序字段t1t2t3t4
  • 添加填充字段
  • payload 上的 gcc 属性 packed
  • 也许是其他人?

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

为了说明问题,如果我们按原样使用上述实现using Foo = four_tuple&lt;char,double,char,double&gt;,我们将有一个 32 的大小用于有效负载和整体。如果我们简单地声明有效载荷packeddouble 将不会很好地对齐。以降序对字段重新排序的模板专业化(此处为double, double, char, char)将提供有效负载和总大小为 24。但它使用的额外 6 个字节是浪费的,从考虑using Bar = four_tuple&lt;Foo::payload,int,int,int&gt; 可以看出。使用最佳包装Bar 可以容纳 32 个字节,但使用此方案需要 40 个。直言不讳地使用packed 应用字段重新排序将导致Bar 中的int 未对齐 - 需要一些填充符。

我知道,由于缓存考虑,通常重组结构字段的内存布局可能会影响性能,并且通常这些影响至少与更好打包的任何潜在收益一样重要。不过,我想探索权衡取舍,如果不解决这个问题,我真的无法在我的上下文中正确地做到这一点。

【问题讨论】:

  • 我没看到。如果按照您描述的方式重新排序,但不添加packed,有什么问题?在另一种类型中使用它不会产生任何未对齐的字段,因为编译器已经使用填充来修复它。
  • 顺便说一句,为什么要强制对齐结构?如果你的类型是四个chars,你想要一个 4 的大小,不是吗?这不能要求对齐 8。
  • @hvd four_tuple&lt;Foo::payload,int,int,int&gt; 如果包装得当,可以达到 32 的大小而不会出现错位。如果你不添加packed,它将有40的大小。
  • @hvd 解决我的问题只保证叶子将相对于four_tuple 对齐。如果four_tuple 的特定实例未对齐,则所有赌注都将取消。这就是我对齐 8 的原因。
  • stackoverflow.com/questions/18975130/… 详细讨论了编译时重新排序以优化打包。答案也参考rmf.io/cxx11/optimal-tuple-i 讨论元组数据布局。

标签: c++ c++11 memory-alignment packed


【解决方案1】:

嵌套元组案例中的大问题是您希望有一个four_tuple&lt;char,double,char,double&gt;::payload 类型的字段,像four_tuple&lt;char,double,char,double&gt; 一样对齐,但不需要容器类型继承其对齐方式。这是复杂的。这样做是可能的,但它会使您的代码非常难以移植到 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&lt;T1, T2, T3, T4&gt; 按顺序检查tuple4_payload&lt;T1, T2, T3, T4, 1234&gt;tuple4_payload&lt;T1, T2, T3, T4, 1243&gt; 等,然后选择最好的。

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&lt;int, tuple4&lt;char, double, char, double&gt;, int, int&gt;。请注意,即使此处未明确提及有效负载类型,它仍将用于t2 成员。

【讨论】:

  • 感谢您的工作。这不适合我,但我想我明白了。如果我理解正确,从tuple4&lt;tuple4&lt;8,4,4,2&gt;, tuple4&lt;2,4,8,8&gt;, 8, 8&gt; 将使用填充空间的意义上讲,它仍然不是最优的,而没有重新排序的天真打包实现将不使用填充并满足基本类型没有错位的要求。 (这里,2=int16_t4=int32_t8=int64_t)。但它应该比我拥有的其他任何东西都好用。
  • @dshin 如果您将conditional_t&lt;...&gt; 更改为conditional&lt;...&gt;::type,它可以与GCC 4.9 及更高版本、4.8 一起编译。是的,你是对的,在你的例子中它不是最优的。由于packed 只能在您想保持与对象开头对齐的情况下删除尾随填充,所以tuple4&lt;2,4,8,8&gt; 被重新排序为tuple4&lt;8,8,4,2&gt;,此时您的首选布局不再可能。抱歉,我没有办法解决这个问题。
  • 理论上我可以想象如果tuple4::payload类型有8个静态bool对应是否可以偏移{0, 1, 2, 3, 4, 5, 6, 7}字节,然后元算法可以将其合并到详尽的搜索中。但是需要一些聪明才智来修剪搜索空间。
  • @dshin 基本上,您最终会得到 8*N+2 的对齐要求,这很棘手,并且无法与任何编译器内置对齐功能一起使用。但其他可能有问题的是,如果不考虑包含元组(如果有),就无法确定元组的理想布局:给定tuple4&lt;tuple4&lt;2,8,8,8&gt;,tuple4&lt;2,8,8,8&gt;,8,8&gt;:理想情况下,您希望将其排序为8,8,8,2,2,8,8,8,8,8,但是意味着第一个 tuple4&lt;2,8,8,8&gt; 和第二个 tuple4&lt;2,8,8,8&gt; 不会有相同的布局。
  • 你说得对,无论上下文如何,payload 布局都必须固定。所以这似乎是最好的解决方案。
猜你喜欢
  • 2021-08-22
  • 1970-01-01
  • 1970-01-01
  • 2013-12-31
  • 2010-11-04
  • 2017-07-24
  • 1970-01-01
  • 2015-03-07
  • 2021-05-25
相关资源
最近更新 更多