使用静态tmp变量为简单类型C++重新实现std::swap()
Reimplementing std::swap() with static tmp variable for simple types C++
我决定为简单类型(如int
、struct
或在字段中仅使用简单类型的class
)的交换函数的实现进行基准测试,其中包含static
tmp变量,以防止在每次交换调用中分配内存。所以我写了一个简单的测试程序:
#include <iostream>
#include <chrono>
#include <utility>
#include <vector>
template<typename T>
void mySwap(T& a, T& b) //Like std::swap - just for tests
{
T tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
template<typename T>
void mySwapStatic(T& a, T& b) //Here with static tmp
{
static T tmp;
tmp = std::move(a);
a = std::move(b);
b = std::move(tmp);
}
class Test1 { //Simple class with some simple types
int foo;
float bar;
char bazz;
};
class Test2 { //Class with std::vector in it
int foo;
float bar;
char bazz;
std::vector<int> bizz;
public:
Test2()
{
bizz = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
}
};
#define Test Test1 //choosing class
const static unsigned int NUM_TESTS = 100000000;
static Test a, b; //making it static to prevent throwing out from code by compiler optimizations
template<typename T, typename F>
auto test(unsigned int numTests, T& a, T& b, const F swapFunction ) //test function
{
std::chrono::system_clock::time_point t1, t2;
t1 = std::chrono::system_clock::now();
for(unsigned int i = 0; i < NUM_TESTS; ++i) {
swapFunction(a, b);
}
t2 = std::chrono::system_clock::now();
return std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count();
}
int main()
{
std::chrono::system_clock::time_point t1, t2;
std::cout << "Test 1. MySwap Result:tt" << test(NUM_TESTS, a, b, mySwap<Test>) << " nanosecondsn"; //caling test function
t1 = std::chrono::system_clock::now();
for(unsigned int i = 0; i < NUM_TESTS; ++i) {
mySwap<Test>(a, b);
}
t2 = std::chrono::system_clock::now();
std::cout << "Test 2. MySwap2 Result:tt" << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count() << " nanosecondsn"; //This result slightly better then 1. why?!
std::cout << "Test 3. MySwapStatic Result:t" << test(NUM_TESTS, a, b, mySwapStatic<Test>) << " nanosecondsn"; //test function with mySwapStatic
t1 = std::chrono::system_clock::now();
for(unsigned int i = 0; i < NUM_TESTS; ++i) {
mySwapStatic<Test>(a, b);
}
t2 = std::chrono::system_clock::now();
std::cout << "Test 4. MySwapStatic2 Result:t" << std::chrono::duration_cast<std::chrono::nanoseconds>(t2 - t1).count() << " nanosecondsn"; //And again - it's better then 3...
std::cout << "Test 5. std::swap Result:t" << test(NUM_TESTS, a, b, std::swap<Test>) << " nanosecondsn"; //calling test function with std::swap for comparsion. Mostly similar to 1...
return 0;
}
Test
定义为Test1
(g++(Ubuntu 4.8.2-19ubuntu1)4.8.2称为g++main.cpp-O3-std=c++11)的一些结果:
测试1。MySwap结果:625105480纳秒
测试2。MySwap2结果:528701547纳秒
测试3。MySwapStatic结果:338484180纳秒
测试4。MySwapStatic2结果:228228156纳秒
测试5。std::swap结果:564863184纳秒
我的主要问题是:使用这个实现来交换简单类型好吗?我知道,例如,如果你用它来交换向量的类型,那么std::swap
会更好,你只需将Test
的定义更改为Test2
就可以看到它。
第二个问题:为什么测试1、2、3和4的结果如此不同?我在测试函数实现方面做错了什么?
首先回答第二个问题:在测试2和测试4中,编译器内联函数,因此它提供了更好的性能(测试4中还有更多内容,但我稍后会介绍)。
总的来说,使用静态温度变量可能是个坏主意。
为什么?首先,需要注意的是,在x86汇编中,没有从内存复制到内存的指令。这意味着当您交换时,CPU寄存器中不是有一个,而是有两个临时变量。这些临时变量必须在CPU寄存器中,不能将mem复制到mem,因此静态变量将添加第三个内存位置以进行传输。
静态临时的一个问题是它会阻碍内联。想象一下,如果您交换的变量已经在CPU寄存器中。在这种情况下,编译器可以内联交换,并且从不将任何内容复制到内存中,这要快得多。现在,如果强制使用静态临时,编译器要么删除它(无用),要么强制添加内存副本。这就是测试4中发生的情况,GCC删除了对静态变量的所有读取。它只是毫无意义地向它写入更新的值,因为你告诉它这样做。读取删除解释了良好的性能增益,但它可能会更快。
你的测试用例是有缺陷的,因为它们没有显示这一点。
现在你可能会问:那为什么我的静态函数表现得更好呢<罢工>我不知道(最后回答)罢工>
我很好奇,所以我用MSVC编译了你的代码,结果发现MSVC做得很好,GCC做得很奇怪。在O2优化级别,MSVC检测到两个交换是非操作的,并将其快捷化,但即使在O1,非内联生成的代码也比在O3使用GCC的所有测试用例更快。(编辑:实际上,MSVC也做得不对,请参阅最后的解释。)
MSVC生成的程序集看起来确实更好,但当比较GCC生成的静态和非静态程序集时,我不知道为什么静态程序集表现更好。
无论如何,我认为即使GCC生成了奇怪的代码,内联问题也应该值得使用std::swap,因为对于较大的类型,额外的内存拷贝可能会很昂贵,而较小的类型可以提供更好的内联。
以下是所有测试用例生成的程序集,如果有人知道为什么GCC静态程序集比非静态程序集表现更好,尽管它更长,使用了更多的内存移动编辑:最后回答
GCC非静态(性能570ms):
00402F90 44 8B 01 mov r8d,双字ptr[rcx]00402F93 F3 0F 10 41 04 movss xmm0,双字ptr[rcx+4]00402F98 0F B6 41 08 movzx eax,字节ptr[rcx+8]00402F9C 4C 8B 0A mov r9,qword ptr[rdx]00402F9F 4C 89 09 mov qword ptr[rcx],r900402FA2 44 0F B6 4A 08 movzx r9d,字节ptr[rdx+8]00402FA7 44 88 49 08 mov字节ptr[rcx+8],r9b00402FAB 44 89 02 mov双字ptr[rdx],r8d00402FAE F3 0F 11 42 04 movss双字ptr[rdx+4],xmm000402FB3 88 42 08 mov字节ptr[rdx+8],al
GCC静态和MSVC静态(性能275ms):
00402F10 48 8B 01 mov rax,qword ptr[rcx]00402F13 48 89 05 66 11 00 mov qword ptr【404080h】,rax00402F1A 0F B6 41 08 movzx eax,字节ptr[rcx+8]00402F1E 88 05 64 11 00 00 mov字节ptr[4040088h],al00402F24 48 8B 02 mov rax,qword ptr[rdx]00402F27 48 89 01 mov qword ptr[rcx],rax00402F2A 0F B6 42 08 movzx eax,字节ptr[rdx+8]00402F2E 88 41 08 mov字节ptr[rcx+8],al00402F31 48 8B 05 48 11 00 mov rax,qword ptr【404080h】00402F38 48 89 02 mov qword ptr[rdx],rax00402F3B 0F B6 05 46 11 00 00 movzx eax,字节ptr[4040088h]00402F42 88 42 08 mov字节ptr[rdx+8],al
MSVC非静态(性能215ms):
00000f2 0f 10 02 movsdx xmm0,QWORD PTR[rdx]00004 f2 0f 10 09 movsdx xmm1,QWORD PTR[rcx]00008 44 8b 41 08 mov r8d,DWORD PTR[rcx+8]0000c f2 0f 11 01 movsdx QWORD PTR[rcx],xmm000010 8b 42 08 mov eax,DWORD PTR[rdx+8]00013 89 41 08 mov DWORD PTR[rcx+8],eax00016 f2 0f 11 0a movsdx QWORD PTR[rdx],xmm10001a 44 89 42 08 mov DWORD PTR[rdx+8],r8d
std::交换版本都与非静态版本相同。
经过一些有趣的调查,我发现了GCC非静态版本性能不佳的可能原因。现代处理器有一个称为存储加载转发的功能。当内存加载与以前的内存存储相匹配时,此功能就会启动,并缩短内存操作以使用已知值。在这种情况下,GCC以某种方式对参数A和B使用非对称加载/存储。A使用4+4+1字节复制,B使用8+1字节复制。这意味着类的前8个字节将不会被存储匹配到加载转发,从而失去宝贵的CPU优化。为了检查这一点,我手动将8+1副本替换为4+4+1副本,性能如预期一样提高(下面的代码)。最后,GCC没有考虑到这一点是有过错的。
GCC修补代码,更长,但利用存储转发(性能220ms):
00402F90 44 8B 01 mov r8d,双字ptr[rcx]00402F93 F3 0F 10 41 04 movss xmm0,双字ptr[rcx+4]00402F98 0F B6 41 08 movzx eax,字节ptr[rcx+8]00402F9C 4C 8B 0A mov r9,qword ptr[rdx]00402F9F 4C 89 09 mov qword ptr[rcx],r900402F9 C 44 8B 0A mof r9d,dword ptr[rdx]00402F9F 44 89 09 mov双字ptr[rcx],r9d00402FA2 44 8B 4A 04 mov r9d,双字ptr[rdx+4]00402FA6 44 89 49 04 mov双字ptr[rcx+4],r9d00402FAA 44 0F B6 4A 08 movzx r9d,字节ptr[rdx+8]00402FAF 44 88 49 08 mov字节ptr[rcx+8],r9b00402FB3 44 89 02 mov双字ptr[rdx],r8d00402FB6 F3 0F 11 42 04 movss双字ptr[rdx+4],xmm000402FBB 88 42 08 mov byte ptr[rdx+8],al
实际上,这个复制指令(对称4+4+1)是正确的方法。在这些测试中,我们只做复制,在这种情况下,MSVC版本无疑是最好的。问题是,在实际情况下,类成员将被单独访问,从而生成4个字节的读/写。MSVC 8字节的批处理副本(也是GCC为一个参数生成的)将阻止单个成员将来的存储转发。我对副本旁边的成员操作进行了一次新的测试,结果表明,修补后的4+4+1版本确实优于所有其他版本。乘以接近x2的因子。遗憾的是,没有现代编译器生成这种代码。
- 有没有比在库中添加一个并非由所有派生类实现的新虚拟函数更好的设计实践
- C++ 如何使用动态计算的新节点实现 A*?
- 实现一个函数,该函数将字符串作为输入并返回一个新字符串,辅音字母不替换为 "!"
- 为什么这个新的 [ ] 和删除 [ ] 实现会分解为 12 >整数?
- 在不创建新节点的情况下实现带有映射的trie
- 为什么要将 swap() 实现为非抛出
- 使用 Qt5 的新信号/插槽实现向滑块发出信号
- 如何实现由TPAINTBOX创建的新组件的OnMousedown,OnMouseUp事件
- 为什么我不能使私人运营商成为新的并使用默认实现?
- 如何向 C++ 中无法访问其实现的类添加新函数
- 在哪里可以找到C 中新运营商的确切实现
- 使用静态tmp变量为简单类型C++重新实现std::swap()
- C++新的不同实现
- 如何实现一个创建新对象并返回对它的引用的C++方法
- 为具有内部指针的对象实现swap()
- 如何阻止gcc在较新的ARM cpu上发出swap{b}
- 如何实现消费者生产者,消费者可以请求新的数据
- 为什么使用 swap 来实现复制分配
- 您将如何重构这种多态设计,以便在添加新实现时使其更加灵活?
- 库函数的新实现,并在其中调用旧实现