如何使用位域成员将 类 的 `swap` 专门化?

How to specialize `swap` for classes with bitfield members?

假设我有一个 C++ 结构:

struct Clazz {
    uint8_t a : 2;
    uint8_t b : 6;
};

我希望能够尽快交换此 class 的元素。只调用 std::swap(cl1, cl2) 还是专门化更好,如何?这行得通吗?

Clazz:swap(Clazz& other) {
    std::swap(a, other.a);
    std::swap(b, other.b); // how to make C++ swap the whole uint8_t value at once?
}

我希望足够的编译器在没有你提供交换的情况下正确地完成它,所以你应该衡量什么会更快但是你可能想尝试的一件事是转换为 uint8_t

void Clazz::swap(Clazz& other) {
    std::swap(reinterpret_cast<uint8_t&>(*this), reinterpret_cast<uint8_t&>(other))
}

或者使用 memcpy

重写交换

好的,现在去编译代码吧。已使用 -O2 优化级别的 gcc5.2 进行测试:

void test1(Clazz& a, Clazz& b) {
  a.swap(b);
}

void test2(Clazz& a, Clazz& b) {
  std::swap(a, b);
}

生成的代码:

test1(Clazz&, Clazz&):
    movzbl  (%rdi), %eax
    movzbl  (%rsi), %edx
    movb    %dl, (%rdi)
    movb    %al, (%rsi)
    ret
test2(Clazz&, Clazz&):
    movzbl  (%rdi), %eax
    movzbl  (%rsi), %edx
    movb    %dl, (%rdi)
    movb    %al, (%rsi)
    ret

DEMO

假设您有以下代码:

struct Clazz {
    uint8_t a : 2;
    uint8_t b : 6;
};

Clazz c1,c2;
c1.a = 1; c1.b = 61;
c2.a = 3; c2.b = 63;

您可以在 class 中只执行 std::swap(c1, c2) 而无需额外的交换功能,事情就会按预期进行交换。请注意,结果可能会因实施和粗略的性能测试而异() shows that a custom swap (in the case of this code) may be more optimal. Thanks to user Nax's ,我在下面编辑了我的测试代码以考虑更大的集合。

当下面的测试代码在没有优化的情况下在 g++ 4.2.1 中编译时,std::swap 循环的粗略平均值变为 0.00523261 usClazz:swap 粗略平均值变为至 0.00996584 us。这意味着 std::swap 实现比 reinterpret_castClazz 对象转换为 uint8_t 更快,但是,当您打开最大优化时(-O3 ),时间更加明显,std::swap 循环进入 0.00102523 usClazz::swap 循环进入 0.000739868 us,速度显​​着提高 std::swap方法。

此外,如果您的 Clazz 对象变得比默认构造函数更复杂,以至于您必须包含一个复制构造函数,那么 std::swap 的时间将几乎翻倍因为大多数实现将在创建临时对象时使用对象的复制构造函数。向 Clazz 对象添加一个空的默认构造函数和一个复制构造函数(即 Clazz(const Clazz& cp) : a(cp.a), b(cp.b) {})将 std::swap 次更改为以下内容:no optimizations ~ 0.0104058 us-O3 ~ 0.00241034 us;翻倍,而 Clazz::swap 方法保持一致。

为此,如果您希望 std::swap 方法调用您的自定义 Clazz::swap 方法,有几种方法可以实现此目的,因为 std::swap 方法不会自动调用任何 classes 定义的 swap 方法(即 std::swap 不调用 a.swap(b))。

您可以直接用您的专用 class 重载 std::swap 方法,例如:

namespace std {
    void swap(Clazz& a, Clazz& b)
    {
        std::swap(reinterpret_cast<uint8_t&>(a), reinterpret_cast<uint8_t&>(b));
        /* or a.swap(b) if your Clazz has private types that need to be
        accounted for and you provide a public swap method */
    }
}

但是,这样做意味着 argument-dependant lookup 找不到此专业化,因此调用 swap(a, b)std::swap(a, b) 不同。

为了满足 ADL,您可以将 swap 方法包含在 Clazz 对象的同一命名空间中(无论是全局命名空间还是 named/anonymous 命名空间),示例:

using std::swap;

namespace ClazzNS {
    struct Clazz {
        uint8_t a : 2;
        uint8_t b : 6;
    };
    
    void swap(Clazz& a, Clazz& b)
    {
        std::swap(reinterpret_cast<uint8_t&>(a), reinterpret_cast<uint8_t&>(b));
        /* or a.swap(b) if your Clazz has private types that need to be
        accounted for and you provide a public swap method */
    }
}

int main()
{
    ClazzNS::Clazz c1,c2;
    c1.a = 1; c1.b = 61;
    c2.a = 3; c2.b = 63;
    swap(c1, c2); // calls ClazzNS::swap over std::swap
}

这只是为了让您可以调用 swap(a, b) 而不必明确命名空间。

如果你比较迂腐,你可以将它们混合在一起得到以下结果:

namespace ClazzNS {
    struct Clazz {
        uint8_t a : 2;
        uint8_t b : 6;

        void swap(ClazzNS::Clazz& other)
        {
            std::swap(reinterpret_cast<uint8_t&>(*this), reinterpret_cast<uint8_t&>(other));
        }
    };
    
    void swap(ClazzNS::Clazz& a, ClazzNS::Clazz& b)
    {
        a.swap(b);
    }
}

namespace std {
    void swap(ClazzNS::Clazz& a, ClazzNS::Clazz& b)
    {
        ClazzNS::swap(a, b);
        /*
        you could also directly just call
        std::swap(reinterpret_cast<uint8_t&>(a), reinterpret_cast<uint8_t&>(b))
        or a.swap(b) if you wanted to avoid
        multiple function calls */
    }
}

int main()
{
    ClazzNS::Clazz c1,c2;
    c1.a = 1; c1.b = 61;
    c2.a = 3; c2.b = 63;
    c1.swap(c2); // calls ClazzNS::Clazz::swap 
    swap(c1, c2); // calls ClazzNS::swap
    std::swap(c1, c2); // calls the overloaded std::swap
}

您的结果可能会有所不同,最终取决于您如何实施交换方法,但最好还是亲自测试一下;我希望这些数字能有所帮助。

C++测试代码:

#include <iostream>
#include <algorithm>
#include <string>
#include <ctime>
#include <csignal>

struct Clazz {
    uint8_t a : 2;
    uint8_t b : 6;
    
    void swap(Clazz& other)
    {
        std::swap(reinterpret_cast<uint8_t&>(*this), reinterpret_cast<uint8_t&>(other));
    }
};

static double elapsed_us(struct timespec init, struct timespec end)
{
    return ((end.tv_sec - init.tv_sec) * 1000000) + (static_cast<double>((end.tv_nsec - init.tv_nsec)) / 1000);
}

static void printall(const Clazz& c1, const Clazz& c2)
{
    std::cout << "c1.a:" << static_cast<unsigned int>(c1.a) << ", c1.b:" << static_cast<unsigned int>(c1.b) << std::endl;
    std::cout << "c2.a:" << static_cast<unsigned int>(c2.a) << ", c2.b:" << static_cast<unsigned int>(c2.b) << std::endl;
}

int main() {
    int max_cnt = 100000001;
    struct timespec init, end;
    Clazz c1, c2;
    c1.a = 1; c1.b = 61;
    c2.a = 3; c2.b = 63;
    printall(c1, c2);
    
    std::cout << "std::swap" << std::endl;
    std::swap(c1, c2); // to show they actually swap
    printall(c1, c2);
    
    std::cout << "c1.swap(c2)" << std::endl;
    c1.swap(c2); // to show again they actually swap
    printall(c1, c2);
    
    std::cout << "std::swap loop" << std::endl;
    clock_gettime(CLOCK_MONOTONIC, &init);
    for (int i = 0; i < max_cnt; ++i) {
        std::swap(c1, c2);
    }
    clock_gettime(CLOCK_MONOTONIC, &end);
    printall(c1, c2);
    // rough estimate of timing / divide by iterations
    std::cout << "std::swap avg. us = " << (elapsed_us(init, end) / max_cnt) << " us" << std::endl;
    
    std::cout << "Clazz::swap loop" << std::endl;
    clock_gettime(CLOCK_MONOTONIC, &init);
    for (int i = 0; i < max_cnt; ++i) {
        c1.swap(c2);
    }
    clock_gettime(CLOCK_MONOTONIC, &end);
    printall(c1, c2);
    // rough estimate of timing / divide by iterations
    std::cout << "Clazz:swap avg. us = " << (elapsed_us(init, end) / max_cnt) << " us" << std::endl;
    
    return 0;
} 

希望能帮到你。