65.9K
CodeProject 正在变化。 阅读更多。
Home

一些有用的工具。

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.74/5 (16投票s)

2014年5月15日

CPOL

8分钟阅读

viewsIcon

15497

downloadIcon

195

轻量级性能分析和测试单元;操作符 = 重载的模板。

引言

您好!我想向您介绍三个对我日常工作非常有帮助的工具(尤其是前两个)。当然,它们可能在本文发表前就已经成功使用了很多年,但我并非这些工具的原创作者。我只想和大家分享它们,就像我给我的学生分享一样。那么,让我们开始吧。

第一个:一个跨平台的执行测量工具

如果您在阅读完这部分后说“这是个老生常谈”,我完全同意。但我就是喜欢我的老生常谈!它们简单、方便、编写速度快,而且当我想比较两个算法来找出哪个更快时,无需学习某个庞大的、包罗万象的工具。在 Windows 下工作时,我习惯使用 QueryPerformanceCounter API 函数,它“检索高分辨率性能计数器的当前值”(引自 MSDN)。但处理跨平台应用程序需要一个更通用的工具。这就是它:

#include <stdint.h>

//this tool is compatible for x86_64 architecture only
#ifdef _WIN32                //  Windows
   #include <intrin.h>

   uint64_t rdtsc()
   {
       return __rdtsc();
   }
#else                        //  Linux/GCC
   uint64_t rdtsc()
   {
      unsigned int lo,hi;
      __asm__ __volatile__ ("rdtsc" : "=a" (lo), "=d" (hi));
      return ((uint64_t)hi << 32) | lo;
   }
#endif //_WIN32 

#define EXPR_TIME_CONS_AVER(nCount, expr, bShowCurTime)\
{\
   char ExprText[sizeof(#expr) + 1] = {0};\
   memcpy(ExprText, #expr, sizeof(#expr));\
   if(bShowCurTime == true)\
     cout<<"=== "<<ExprText<<" === start"<<endl;\
   uint64_t ui1, ui2, uiTicks, uiAverage = 0;\
   for(int iIn = 0; iIn < nCount; ++iIn)\
   {\
      ui1 = rdtsc();\
      expr;\
      ui2 = rdtsc();\
      uiTicks = ui2 - ui1;\
      uiAverage += uiTicks;\
      if(bShowCurTime == true)\
         cout<<uiTicks<<endl;\
   }\
   cout<<"=== "<<ExprText<<" average == "<<uiAverage / nCount<<"\n\n";\
}

这是一个宏。尽管我们无法调试它们,但它们仍然有很多优点(事实上,我们可以通过将宏代码放入一个函数中来简单地调试宏)。

  1. 我们可以将整个指令传递给它。不像函数参数那样必须有某种类型,而是可以传递任何指令甚至多个指令。
  2. # 操作符可以保存任何文本(别忘了:任何代码只是纯文本),并使我们能够将传递的指令像常规的 const char * 一样处理。

我来解释一下这段代码中每一个棘手的行

rdtsc(); - 一个封装函数,返回 CPU 时钟周期的数量。
#define EXPR_TIME_CONS_AVER(nCount, expr, bShowCurTime)

这是宏的定义,旨在在控制台中显示平均执行时间。第一个参数 nCount – 要执行传递的指令多少次来计算平均执行时间(此处在 for(int i;… 循环中)。第二个 expr – 将在循环中执行的指令。第三个 bShowCurTime – 负责在控制台窗口显示每次迭代的当前执行时间。这对于我们想大致了解纯执行时间很有帮助,因为如果在执行我们的指令期间另一个线程干扰并切换了上下文,时间会急剧增加。(您可以在此宏中添加其他参数——一个 uint64_t 数组及其大小,用于在循环外部处理时间并排除那些明显不按顺序的值。)

char ExprText[sizeof(#expr) + 1] = {0};
这里我们有:#expr – 将传递的指令视为 const char *sizeof(#expr) + 1 – sizeof 操作符可以在编译时计算字符数组(我们的指令)的大小,因此我们可以声明另一个字符数组 ExprText(作为缓冲区)在栈上。+1 是为了在其末尾放置 ‘\0’ = {0}。(以便使用方便的 c/c++ 工具输出它)char ExprText – 我将此数组包含在代码中只是为了提示:如何将此字符串从宏中返回以进行任何方式的操作。您只能在宏中使用 # 操作符。例如:
  1. 让我们比较一下在“for”循环中逐元素复制 int 数组与 memcpy 函数的工作。
  2. 哪个更快?复制构造函数有 9 行汇编代码,还是移动构造函数有 16 行?(在 gcc 中,您需要向编译器参数添加 c++11 支持)。在 Array.cpp 中
Array::Array(const Array & Right): m_nSize(Right.m_nSize),
       m_nReadonly(Right.m_nReadonly)
{
   m_pAr = new int[m_nSize];
   memcpy(m_pAr, Right.m_pAr, sizeof(int) * m_nSize);

}

Array::Array(Array && Right) noexcept : m_nSize(Right.m_nSize),
       m_pAr(Right.m_pAr), m_nReadonly(Right.m_nReadonly)
{  
     Right.m_nSize = 0;
     Right.m_pAr = 0;

}

在 main 中

#define ARR_SIZE 2000
int iAr[ARR_SIZE], iAr2[ARR_SIZE];

for(int i = 0; i < ARR_SIZE; ++i)
   iAr[i] = rand();

int nSamplesCount = 3;
EXPR_TIME_CONS_AVER(nSamplesCount,
for(int j = 0; j < ARR_SIZE; ++j) iAr2[j] = iAr[j], true);

EXPR_TIME_CONS_AVER(nSamplesCount,
memcpy(iAr2, iAr, sizeof(int) * ARR_SIZE),  true);

cout<<endl;

Array ar(10);
EXPR_TIME_CONS_AVER(nSamplesCount, Array ar2(ar), false);
EXPR_TIME_CONS_AVER(nSamplesCount, Array ar3(std::move(ar)), false);

输出 (gcc 4.8.0)

试试看,您会发现很多有趣且意想不到的事情,尤其是在使用不同编译器时(相信我!)。

第二个工具: **使用预处理器编写简单的测试单元。**

自从我成为自由职业者以来,我就开始使用这个技巧。它只是预处理器的一个便捷功能,我用它来快速打开或关闭我(不太大的)项目中的测试代码。现在我也用它来让我的学生习惯于与 assert、异常处理或写入文件一起检查所有有疑问的数据。Boost.testCppUnit 非常好,但它们需要大量时间来掌握。这里有一个更简单的变体。看:

我们有两种方式来声明自己的指令:作为宏

#define Test_FuncTime(param) param;

以及作为指令本身 - 而不将其展开为任何代码或值

#define Test_FuncTime(param)

如果我们传递一个指令或一个函数调用作为参数,它可以被展开为宏本身的代码,或者什么都没有。所以,我总是成对编写这些定义。其中一个必须被注释掉。如果我想在我的应用程序中测试某些内容,我会编写一些批准代码,这实际上会弄乱程序代码并影响其可读性。因此,当需要测试时 - 我会取消注释宏,当我想隐藏这些额外的输出时 - 我会注释掉宏并同时取消注释空定义。在我看来,这是一个非常方便的工具。

让我们看看它展开后仍然简单的用法:要用测试覆盖任何新任务,您只需复制粘贴这些声明,并根据任务编号或类似名称给它们起一个合适的名字。(测试指令名称可以包含,例如,该大项目模块的名称。)如果我们想要一种广泛的测试,我们可以编写一个具有所需任意数量参数的函数,并从宏中调用它。我喜欢以下 c++ 代码组织:我有一个用于任务描述的头文件,一个用于测试声明(#define Test#123(a) a;)的头文件,并附有良好的注释,总体上描述了我想要测试的内容。接下来,因为一个编译单元(一个 *.cpp 文件)包含了编写代码的所有 include,所以在文件顶部的它们之后,在一个单独的区域声明所有原型。在文件末尾 - 定义所有函数,并详细描述我在这里必须收到什么!!我会在每个特定函数中这样做,以便在再次返回它时快速记住所有内容。如果您想收集一些数据并将其传递给测试的另一部分 - 没问题。如果您的应用程序是跨平台的,并且您想在程序输出中而不是调试器中看到测试数据 - 使用模式桥来输出信息。也就是说:抽象的基类输出,以及每个特定平台的几个子类。就这些。

我喜欢我的测试中的另一个特点——它们就像一行代码一样放在代码中,我将它们放在第 80 列之后——这样在工作和思考时就不会看到它们。这是我的代码中测试的实际位置:

某些方法定义(如上面的第 19 和第 27 个)末尾的空行对我来说是提示——“这里有什么”。如果您想看到它们——只需向右滚动,或者查看您的测试覆盖日志——哪些任务被测试覆盖了。

我们来看一个例子(所有例子都包含在文章的 zip 文件中。)

// tests.h 
// test for trace all stages of class Array functionality:

// constructors, destructors and so on#define Test123_ClassArray(a) a;     
//#define Test123_ClassArray(a) 
// array.cpp

Array::~Array()
{   
    delete [] m_pAr; 
    // I push all test macroses to the right at >= 80-th column
    Test123_ClassArray(cout<<"~Array()"<<this<<' '<<m_nSize<<endl);
}

当然,这不是一个真正的测试。通过这种方式,我向学生解释了对象的生命周期以及何时调用了哪些类功能(构造函数、析构函数等)。因此,这些测试包含在每个研究过的地方(在每个文件中)。您可以打开它们

#define Test123_ClassArray(a) a;
//#define Test123_ClassArray(a) 

然后在 main 中看到:

{
       std::vector<Array> vc;
       int nCount = 2;
       for(int i = 0; i < nCount; ++i)
       {
           Test123_ClassArray(cout<<"-- before push: size cap "\
           <<vc.size()<<' '<<vc.capacity()<<endl);
           vc.push_back(Array(rand()% 21));
           Test123_ClassArray(cout<<"-- after push: size cap "\
           <<vc.size()<<' '<<vc.capacity()<<"\n\n");
       }
 
       for(int i = 0; i < nCount; ++i)
           vc[i].Show();
}

这将产生

或者关闭它们

//#define Test123_ClassArray(a) a;
#define Test123_ClassArray(a) 

并只接收

最后一个技巧:操作符 = 重载的模板。

如果我们分配了资源并且不反对允许类的用户调用它,我们必须重载 operator =。(我们可以禁止使用此运算符。)经典的动作序列:

  1. 检查 – 我们是否在将一个对象分配给自己?;
  2. 分配与右操作数完全相同的资源;
  3. 释放我们自己的资源;
  4. 深度复制;
  5. 返回 *this

但是……如果类有一个 const 成员——所谓的 readonly 字段呢?(C# 有这个关键字)如果您想在运行时设置成员变量并保证它在对象生命周期内保持不变,这是一个方便的工具。

class Array
{
    int m_nSize;
    int * m_pAr;
    const int m_nReadonly;

在这种情况下,在我们的重载的 operator = 中任何尝试重新赋值的行为都会让我们的编译器生气。有两种解决方案:1)遵循该工具的意图——如果您使用 const 成员——就不要更改它!!!您必须重新设计您的程序。2)如果这种重新赋值不是一种变通方法,而是完全符合您的程序逻辑,您必须记住复制构造函数。

我故意在重载序列中将分配新资源放在释放资源之前。这与异常和事务安全性有关——我们保证左操作数无论如何都会拥有资源。(如果根据逻辑,我们的左操作数可以选择:存储旧资源或接收新资源。)因此,我们将使用复制构造函数来创建一个临时对象,因为:

  1. 我们将以这种方式创建一个新资源,然后再决定删除旧资源。
  2. 在重载的 operator = 中,我们将执行深度复制,这在复制构造函数中已经做得很好。因此,我们将重用现有代码。(顺便说一句,通常,如果我们决定允许使用对象的复制构造,我们也会为用户提供赋值的可能性,反之亦然。)
  3. 这是重新分配 const 成员的完全合法的方法。(无需 hack。)第一种变体:
Array & Array::operator = (const Array & Right)
{
   if(this != &Right)
   {
      Array temp(Right);
      char sBuff[sizeof(Array)];
      memcpy(sBuff, this, sizeof(Array));
      memcpy(this, &temp, sizeof(Array));
      memcpy(&temp, sBuff, sizeof(Array));
   }

   return *this;
}

在这里,我创建了 Right 对象的深度副本,创建了一个缓冲区 sBuff 来交换左操作数(*this)与 temp,并执行交换本身。在闭合括号“}”处,temp 将会销毁,就像任何在栈上分配的变量一样,并实际解决了我们旧资源的所有问题。

这个动作序列对于所有类都是通用的,所以让它成为一个模板:

template <typename T>
void TSwap(T * pThis, const T & Right)
{
   T temp(Right);
   char sBuff[sizeof(T)];
   memcpy(sBuff, pThis, sizeof(T));
   memcpy(pThis, &temp, sizeof(T));
   memcpy(&temp, sBuff, sizeof(T));
}

现在修改后的 operator = 将如下所示:

Array & Array::operator = (const Array & Right)
{
   if(this != &Right)    
      TSwap(this, Right);

   return *this;
} 

所有功能都在 Windows XP、7 的 VS2010 和 VS2012 上进行了测试(使用这些编译器时,您必须注释掉 class Array 移动构造函数中的 noexcept 关键字,因为 Microsoft 对 c+11 标准的支持不完整)。以及在 Ubuntu 14.04 上使用 gcc 4.8.2(这里一切都如 .zip 文件所示工作)。

祝您好运!!

© . All rights reserved.