【问题标题】:How to add a bool into a struct without increasing sizeof (in case that there is padding in the struct)?如何在不增加 sizeof 的情况下将 bool 添加到结构中(如果结构中有填充)?
【发布时间】:2019-04-09 03:27:54
【问题描述】:

我正在创建一个哈希表。哈希表的一个节点存储一个 KEY、一个 VALUE 和一个标志(使用与否):

template <typename KEY, typename VALUE>
struct Node {
    union { KEY key; };
    union { VALUE value; };
    bool used;

    Node() { }
};

keyvalue 在一个联合中,因为它们仅在 Node 实际使用时创建(这很重要)。

现在,假设 KEY 或 VALUE 中有一些填充。然后将used 放入填充区域是明智的(因此Node 可以更小)。

是否可以(自动)执行此操作?

如果一般不可以,如果KEY或者VALUE有tail-padding的话,是否可以这样做?


注意,我知道如何利用尾部填充,但是它具有未定义的行为。基本上,这个想法是从 KEY 或 VALUE 派生,并将 bool used 放在那里(在这种情况下,如果对象具有非标准布局,当前的编译器会将 bool used 放在尾部)。但不幸的是,used 在实际创建 KEY 或 VALUE 之前不能使用(new'd) - 这是一个问题,因为当创建一个空节点时,不会创建 KEY 或 VALUE(它是一个开放寻址哈希表,空节点存在)。

注意2:我只在没有特殊的 empty KEY 或 VALUE 值时使用这种方法。如果 KEY 或 VALUE 有一个特殊的 empty 值,那么我当然不需要单独使用bool used

【问题讨论】:

  • 我认为没有一种安全的方法可以适用于任何 KEY 和 VALUE 类型。作为一个简单的替代方案,如何分配一个单独的打包位数组(或std::bitset,如果您愿意)并将所有布尔/使用值存储在那里?这只会使每个节点对象的 Hashtable 的内存使用量增加大约 1 位。
  • @JeremyFriesner:感谢您的建议。单独的位数组方法的问题在于它被分配到其他地方,因此对于大型哈希表,这意味着额外的缓存未命中。一种可能的解决方法(对于小的 KEY/VALUE 元素)是将 8/16/32 位标志交错放入表中。但是,它使实现变得复杂且速度较慢。

标签: c++ hashtable


【解决方案1】:

所以我想了很多,意识到虽然不能使用存储对象内部的填充,但可以尝试安排Node的成员,使它们占用最少的空间。当然,您可以对其进行硬编码,但我认为更优雅的方式是使其自动化。

要找到最小的排列,我们需要生成所有可能的排列并选择最小的排列。生成所有排列可以使用元函数permutations 完成,它生成一组类型的所有排列:

template <typename P1, typename P2>
struct merge {};

template <template <typename...> class P, typename... Ts, typename... Us>
struct merge<P<Ts...>, P<Us...>> {
    using type = P<Ts..., Us...>;
};

template <typename T, typename P>
struct prepend {};

template <typename T, template <typename...> class P, typename... Packs>
struct prepend<T, P<Packs...>> {
    using type = P<typename merge<P<T>, Packs>::type...>;
};

// N is the number of rotations to go
template <std::size_t N, typename Pack, typename = void>
struct permutations_impl {};

template <template <typename...> class P, typename... Ts>
struct permutations_impl<0, P<Ts...>> {
    // All rotations done, break the recursion
    using type = P<>;
};

template <std::size_t N, template <typename...> class P, typename T>
struct permutations_impl<N, P<T>> {
    using type = P<P<T>>;
};

template <std::size_t N, template <typename...> class P, typename F, typename... Rest>
struct permutations_impl<N, P<F, Rest...>, std::enable_if_t<(sizeof...(Rest) && N != 0)>> {
    using PermuteRest = typename permutations_impl<sizeof...(Rest), P<Rest...>>::type;
    using NextRotation = typename permutations_impl<N-1, P<Rest..., F>>::type;

    using type = typename merge<typename prepend<F, PermuteRest>::type, NextRotation>::type;
};

template <typename Pack>
struct permutations {};

template <template <typename...> class P, typename... Ts>
struct permutations<P<Ts...>> {
    using type = typename permutations_impl<sizeof...(Ts), P<Ts...>>::type;
};

要选择所有排列中最小的,我们可以这样做:

template <typename Pack>
struct min_size_element {
    static constexpr std::size_t min_index(std::initializer_list<std::size_t> l) {
        std::size_t min = *l.begin();
        std::size_t idx = 0, result = 0;
        for(auto it = l.begin(); it != l.end(); ++it, ++idx) {
            if(*it < min) min = *it, result = idx;
        }
        return result;
    }
};

template <typename... Ts>
struct min_size_element<std::tuple<Ts...>> : min_size_element<void> {
    using type = std::tuple_element_t<min_index({sizeof(Ts)...}), std::tuple<Ts...>>;
};

template <typename... Ts>
struct smallest_tuple {
    using tuples = typename permutations<std::tuple<Ts...>>::type;
    using type = typename min_size_element<tuples>::type;
};

Node 类随后会将键、值和使用的标志存储在由smallest_tuple 选择的元组中。元素需要按类型访问(因为我们不知道它们的索引),因此键和值元素需要包装在唯一的包装器类型中。 Node 类的实现,其中包含键和值的包装器和访问器,可能如下所示:

template <typename K, typename V>
class Node {
    union KeyWrap {
        struct{} _;
        K key;

        constexpr KeyWrap() : _() {}
    };
    union ValueWrap {
        struct{} _;
        V value;

        constexpr ValueWrap() : _() {}
    };
    // Need to wrap key and value types because tuple elements are accessed by type
    using Storage = typename detail::smallest_tuple<bool, KeyWrap, ValueWrap>::type;

    Storage mStorage;

public:
    constexpr Node() = default;

    ~Node() {
        if(this->used()) {
            this->key().~K();
            this->value().~V();
        }
    }

    Node& operator=(std::pair<K, V> entry) {
        if(this->used()) {
            this->key() = std::move(entry.first);
            this->value() = std::move(entry.second);
        } else {
            new(&std::get<KeyWrap>(mStorage).key) K(std::move(entry.first));
            new(&std::get<ValueWrap>(mStorage).key) V(std::move(entry.second));
            std::get<bool>(mStorage) = true;
        }
        return *this;
    }

    bool used() const {
        return std::get<bool>(mStorage);
    }

    K& key() {
        assert(this->used());
        return std::get<KeyWrap>(mStorage).key;
    }

    V& value() {
        assert(this->used());
        return std::get<ValueWrap>(mStorage).value;
    }
};

Live demo


我还尝试自动检测一个类型是否有一些填充,但不能可靠地做到这一点,并且只能用于可以派生的类型。看看我的回答here

【讨论】:

  • 感谢您的回答。对于这个特定的问题,我认为如果我只是交换键/值顺序,如果键具有较小的对齐方式会提供相同的结果。
  • 你还得看尺寸。
  • 嗯。你能举个反例吗,哪里行不通?
  • 我在写那条评论时以为我有一个,但现在我认为你是对的。您需要将具有最小对齐方式的两个成员打包在一起,如果有空间放置布尔值会更好,或者它没有任何区别。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-12-20
  • 1970-01-01
  • 2011-05-10
  • 2020-05-02
  • 1970-01-01
  • 2020-10-01
相关资源
最近更新 更多