Tommy读书笔记

正在加载...
 
  • 共38文章
  • <
  • 1
  • 2
tree template[code]  

#pragma once
#ifndef TREE_H
#define TREE_H

#include<new>
#include<cassert>

namespace TY_TREE
{
 template<typename T>
 class tree
 {
 private:
//
  //元素结构
  class tree_element
  {
  public:
   T element_value;
   tree_element *left_child;
   tree_element *right_child;
  public:
   tree_element():element_value(T()),left_child(NULL),right_child(NULL){}
   tree_element( const T& t):element_value(t),left_child(NULL),right_child(NULL){}
  };
  typedef tree_element* pointer;
  pointer      root;
//
 public:
  tree():root(NULL){}//default constructor
  tree( const T& t ):root(new( std::nothrow ) tree_element(t)){}//overload constructor
  //~tree();//destructor
 private:
  tree(const tree&);//copy constructor
  tree<T>& operator=(const tree<T>&);
 public://interface
  pointer    Insert (const T&);
  void    Delete (const T&);
  pointer    Find  (const T&);
  void       Release ();
  void    PostInoderTravel();
 private://implement
  pointer    _Insert (const T&, pointer&);
  bool    _Delete (const T&, pointer&);
  pointer&   _Find  (const T&, pointer&);
  pointer&   _FindMin (pointer&);
  pointer&   _FindMax (pointer&);
 private://
  void    _PostInoderTravel  (pointer);
  void    _Release    (pointer&);
 };
//
 template<typename T>
  void tree<T>::Release()
 {
  _Release(root);
 }
//
 template<typename T>
  void tree<T>::PostInoderTravel()
 {
  _PostInoderTravel(root);
 }
//
 template<typename T>
  typename tree<T>::pointer tree<T>::Insert(const T& _value)//builder
 {
  return _Insert(_value, root);
 }
//
 template<typename T>
  typename tree<T>::pointer tree<T>::Find(const T& _value)
 {
  return _Find(_value,root);
 }
//
 template<typename T>
  void tree<T>::Delete(const T& _value)
 {
  _Delete(_value, root);
 }
//
 template<typename T>
  typename tree<T>::pointer tree<T>::_Insert (const T& _value, pointer& pt)//加typename的目的是为了说明pointer 是一个类型
 {
  if (NULL == pt)
  {
   pt = new(std::nothrow) tree_element(_value);
#ifndef DEBUG
   assert(pt != NULL);
#endif
   return pt;
  }
  if (_value <  pt->element_value) 
  {
   return _Insert( _value, pt->left_child );
  }
  if (_value >  pt->element_value)
  {
   return _Insert( _value, pt->right_child );
  }
  if (_value == pt->element_value) 
  {
   return pt;
  }
  return NULL;
 }
//
 template<typename T>
  typename tree<T>::pointer& tree<T>::_Find(const T &_value, pointer &pt)
 {
  if (NULL == pt)
  {
   return pt;
  }
  if (_value < pt->element_value)
  {
   return _Find(_value, pt->left_child);
  }
  if (_value > pt->element_value)
  {
   return _Find(_value, pt->right_child);
  }
  if (_value == pt->element_value)
  {
   return pt;
  }
 }
//
 template<typename T>
  bool tree<T>::_Delete(const T &_value, pointer &pt)
 {
  pointer &temp_pt = _Find(_value, pt);
  if (NULL == temp_pt)
  {
   return false;
  }
  if (temp_pt->left_child && temp_pt->right_child)//当该节点有2个孩子
  {
   pointer &_temp_pt = _FindMin(temp_pt->right_child);
   temp_pt->element_value = _temp_pt->element_value;//iostream不能使用
   _Delete(_temp_pt->element_value,_temp_pt);
   return true;
  }
  else//当该节点只有1个或则0个孩子的时候
  {
   pointer &ref_temp = temp_pt;
   pointer value_temp = temp_pt;
   if (temp_pt->left_child ==NULL)
   {
    temp_pt = temp_pt->right_child;
    delete value_temp;
    ref_temp = NULL;
   }
   else if (temp_pt->right_child ==NULL)
   {
    temp_pt = temp_pt->left_child;
    delete value_temp;
   }
   return true;
  }
 }
//
 template<typename T>
  typename tree<T>::pointer& tree<T>::_FindMin(pointer &pt)
 {
  if(NULL == pt)
  {
   return pt;
  }
  if (NULL == pt->left_child)
  {
   return pt;
  }
  return _FindMin(pt->left_child);
 }
//
 template<typename T>
  typename tree<T>::pointer& tree<T>::_FindMax(pointer &pt)
 {
  if(NULL == pt)
  {
   return NULL;
  }
  if (NULL == pt->right_child)
  {
   return pt;
  }
  return _FindMin(pt->right_child);
 }
//
 template<typename T>
  void tree<T>::_PostInoderTravel(pointer pt)
 {
  if (NULL != pt)
  {
   _PostInoderTravel(pt->left_child);
   _PostInoderTravel(pt->right_child);
   std::cout<<pt->element_value;
  }
  return;
 }
//
 template<typename T>
  void tree<T>::_Release(pointer& pt)
 {
  if (NULL != pt)
  {
   _Release(pt->left_child);
   _Release(pt->right_child);
   delete pt;
   pt = NULL;
  }
 }


 typedef tree<int> ty_tree;
}
#endif

浏览数(29) |  评论数(0) | 03-06 14:06
尾递归 之浅见[载]  

所谓尾递归是指递归调用处于子程序中最后的位置,其后面没有其它操作. 由于这一特点,递归调用的返回值
在其返回后不会被用到. 因此,编译器优化时,很容易把尾递归调用替换成非递归.
 
(1) 如果递归调用的返回值在其返回后要被用到,多数情况是用于表达式计算,这个表达式在递归调用返回之前不能完成计算,那么必须层层压栈,当遇到递归终止条件时,再层层出栈计算.
 
(2) 如果递归调用的返回值在其返回后不被用到,即递归调用是函数的最后一条语句(不被其它表达式语句所包含),这样虽然递归调用了,但是当遇到递归终止条件,层层出栈返回时,不进行任何的表达式计算,仅仅把结果值传回;这就跟迭代非常类似了(迭代就是层层计算,遇到终止条件返回结果),所以编译器可以把这种递归调用转换成迭代,这就是尾递归.
 
 
递归法求1~n的和

(1)普通递归
int fun(int n){
    if(n==0) return 0;
    return n+fun(n-1); /*fun(n-1)的返回值还要用于表达式n+?的计算*/
}
: fun(9);

(2)尾递归
int fun(int sum,int n){
    if(n==0) return sum;
    return fun(sum+n,n-1); /*fun的返回值在其返回后没被用到*/
}
:fun(0,9);

两种递归比较

汇编代码
(1) 普通递归
不优化
fun:
    ...
    cmpl    $0,8(%ebp)    #n-0
    jne    .L2        #不等于0?跳.L2
    movl    $0,-4(%ebp)    #临时变量,用于保存计算结果(返回值)
    jmp    .L1
.L2:
    movl    8(%ebp),%eax    #取n
    decl    %eax        #n-1
    movl    %eax,(%esp)    #n-1值压栈,传给fun的参数
    call    fun        #递归调用fun
    addl    8(%ebp),%eax    #求n+fun(n-1)
    movl    %eax,-4(%ebp)    #保存
.L1:
    movl    -4(%ebp),%eax    #取fun的返回值
    leave            #恢复栈针
    ret            #函数返回

优化后
fun:
    ...
    xorl    %eax,%eax    #寄存器eax清零
    movl    %ebx,-4(%ebp)    #保存ebx的值(惯例:eax,edx,ecx由调用者保存,而其它的有被调函数保存)
    movl    8(%ebp),%ebx    #取n
    testl    %ebx,%ebx    #测试n (n&n)
    jne    .L2        #非0?.L2
.L1:
    movl    -4(%ebp),%ebx    #恢复ebx的值
    leave
    ret
.L2:
    leal    -1(%ebx),%edx    #n-1的值存到edx
    movl    %edx,(%esp)    #n-1的值压栈(fun的参数)
    call    fun        #递归调用fun
    leal    (%eax,%ebx),%eax    #计算n+fun(n-1)的值,作为fun的返回值
    jmp    .L1

(2) 尾递归
不优化
fun:
    ...
    cmpl    $0,12(%ebp)    #n-0
    jne    .L2        #不等于0?跳.L2
    movl    8(%ebp),%eax    #取sum
    movl    %eax,-4(%ebp)    #保存sum到临时变量
    jmp    .L1
.L2:
    movl    12(%ebp),%eax    #取n
    decl    %eax        #n-1
    movl    %eax,4(%esp)    #n-1值压栈(fun的第二个参数)
    movl    12(%ebp),%eax    #取sum
    addl    8(%ebp),%eax    #求sum+n
    movl    %eax,(%esp)    #sum+n值压栈(fun的第一个参数)
    call    fun        #递归调用fun
.L1:
    movl    -4(%ebp),%eax    #取出保存在临时变量里的sum值,作为返回值
    leave
    ret

优化后 (已消除递归调用)
fun:
    ...
    movl    8(%ebp),%eax    #取sum (第一个参数)
    movl    12(%ebp),%edx    #取n    (第二个参数)
.L1:
    testl    %edx,%edx    #测试n是否为0
    je    .L2        #0?.L2
    addl    %edx,%eax    #sum+=n
    decl    %edx        #n--
    jmp    .L1        #跳.L1,循环
.L2:    
    leave
    ret

 

浏览数(50) |  评论数(0) | 03-05 11:00
msdn里对operator new的描述  

void* operator new(
   std::size_t _Count
) throw(bad_alloc);
void* operator new(
   std::size_t _Count,        
   const std::nothrow_t&
) throw( );
void* operator new(
   std::size_t _Count, 
   void* _Ptr
) throw( );//第二个参数是一个指定的指针,已经显示的分配了一块区间,new只要在这块空间上构造就可以了。
char x[sizeof( MyClass )];
MyClass* fPtr2 = new( &x[0] ) MyClass;
fPtr2 -> ~MyClass();
cout << "The address of x[0] is : " << ( void* )&x[0] << endl;
B TTT;
TTT.~B();这种情况下 析构函数会被调用两次;不知道怎么解释。

浏览数(47) |  评论数(0) | 03-02 00:23
static成员  

static 是c++中很常用的修饰符,它被用来控制变量的存储方式和可见性,下面我将从 static 修饰符的产生原因、作用谈起,全面分析 static 修饰符的实质。

static 的两大作用:

一、控制存储方式:

   static被引入以告知编译器,将变量存储在程序的静态存储区而非栈上空间。

  1、引出原因: 函数内部定义的变量,在程序执行到它的定义处时,编译器为它在栈上分配空间,大家知道, 函数在栈上分配的空间在此 函数执行结束时会释放掉,这样就产生了一个问题: 如果想将 函数中此变量的值保存至下一次调用时,如何实现?
最容易想到的方法是定义一个全局的变量,但定义为一个全局变量有许多缺点,最明显的缺点是破坏了此变量的访问范围(使得在此 函数中定义的变量,不仅仅受此 函数控制)。

  2、 解决方案:因此c++ 中引入了 static,用它来修饰变量,它能够指示编译器将此变量在程序的静态存储区分配空间保存,这样即实现了目的,又使得此变量的存取范围不变。

二、控制可见性与连接类型 :

   static还有一个作用,它会把变量的可见范围限制在编译单元中,使它成为一个内部连接,这时,它的反义词为”extern”.

   static作用分析总结: static总是使得变量或对象的存储形式变成静态存储,连接方式变成内部连接,对于局部变量(已经是内部连接了),它仅改变其存储方式;对于全局变量(已经是静态存储了),它仅改变其连接类型。

类中的 static 成员

一、出现原因及作用:

  1、需要在一个类的各个对象间交互,即需要一个数据对象为整个类而非某个对象服务。

  2、同时又力求不破坏类的封装性,即要求此 成员隐藏在类的内部,对外不可见。

  类的 static 成员满足了上述的要求,因为它具有如下特征:有独立的存储区,属于整个类。

二、注意:

  1、对于静态的数据 成员,连接器会保证它拥有一个单一的外部定义。静态数据 成员按定义出现的先后顺序依次初始化,注意静态 成员嵌套时,要保证所嵌套的 成员已经初始化了。消除时的顺序是初始化的反顺序。

  2、类的静态 成员 函数是属于整个类而非类的对象,所以它没有this指针,这就导致了它仅能访问类的静态数据和静态 成员 函数

浏览数(39) |  评论数(0) | 02-29 11:30
union的使用  

使用union结构可以屏蔽我们内部的一些实现,而露出的接口是用户可以理解的。

比如:union obj{

                union obj * free_list_link;

                char client_data[1];

         }

 

         char * func(obj* param1){ return param1; }//只有设计者知道是这样应用转换的,而用户只会知道转换成了一个char*;

浏览数(39) |  评论数(0) | 02-29 10:05
c语言的位字段语法  

通常使用位运算的方法是定义一个与相应位位置有关的掩码集,如:

#define KEYWORD 01

#define EXTERNAL 02

或者 enum { KEYWORD = 01, EXTERNAL = 02 };

这些数字必须是2的某个乘幂。从而访问这些位就 变成了使用位移,掩码,按位求反的简单运算符操作;

而C有另一种方法取而代之,就是提供字内直接定义和访问字段的能力。位字段简称字段,是“字”中相邻位的集合,所谓“字”是由实现定义的单一存储单元。字段定义和访问的语法基于结构。

 

上面的语句还可以这样定义:

struct {

     unsigned int is_keyword    :1;

     unsigned int is_extern       :1;

} flag;//c++语法中也预留了直接在类型后面定义对象的功能,但是不推荐使用,会造成语义的不好理解。

以上定义了一个变量flag,包含了2个1位字段。冒号后面的数字标识了域宽。字段显示的声明为无符号量。字段的使用方法与结构成员相似。

比如 flag.is_keyword = flag.is_extern = 1;等操作。无名字段用于填充,宽度0的字段强制再下一个字边界上对其(不知什么意思)

比如

struct {

     unsigned int first               :1;

     unsigned int second          :1;

     unsigned int                     :3;

     unsigned int fifth               :1;

} flag;//

浏览数(50) |  评论数(0) | 02-29 00:48
stl::allocate之结构简单剖析  

分析一个事物要先从复杂的方面入手,复杂的东西解决了,简单的就是小菜,迎刃而解。这里的剖析不是我看stl源码做出的剖析,而是我看了侯捷的源码剖析后自己分析总结的,如果想了解allocator的人,还是去看书,别看我的。

要分析最复杂的事物就从其所谓次级配置器入手,这是一个为了避免太多小额区块造成内存利用率和效率下降的机制。而sgi的做法是让大于128bytes的内存申请交给malloc函数直接完成,小于128bytes的交给次级配置器管理。

次级配置器有这么几个数据结构用来管理和分配空间以及存储数据: 1 内存池,从system heap用malloc申请与分配的大量空间放置在内存池中。  2 自由链表,使用一个存储着指向多个链表指针元素的链表来当索引,得到匹配的值就分配某个自由链表下的空间,解释得不是很清楚。

内存池要做这么几件事情: 1 第一次分配的时候初始化内存池大小; 2 当refill函数向内存池申请空间的时候,内存池的空间还够分配给客户和freelist的时候就直接分配,就是把内存池的头指针后移; 3 如果内存池的空间不够预分配,但是够分配给客户,就把余下的空间分配给客户; 4 如果内存池的空间连用户都不能分配就直接再malloc一片空间。

浏览数(28) |  评论数(0) | 02-28 15:24
C++ Type traits[转载]  

C++ Type traits

by John Maddock and Steve Cleary
DDJ 2000/10

 

译者:陈崴

侯捷注:本文系北京《程序员》杂志 2001/06 的文章。译笔顺畅,技术饱满。
承译者陈崴先生与《程序员》杂志负责人蒋涛先生答允,
转载於此,以飨台湾读者,非常感谢。

未得陈崴先生与蒋涛先生二人之同意,任何人请勿将此文再做转载。


泛型编程编出来的代码,适用於任何「吻合某种条件限制」的资料型别。这已成为撰写可复用代码时的一个重要选择。然而,总有一些时候,泛型不够好 ─ 有时候是因为不同的型别差距过大,难以产生一致的泛化实作版本。这个时候 traits 技术就变得相当重要。这种技术可以将那些需要被纳入考量的型别性质以一种 type by type 的原则,封装於一个 traits class 内,於是可以将「由於型别之间的差异,必须撰写出来以备用」的代码体积降至最低,并使泛用代码的体积提升到最高。

考虑一个例子:当我们处理字元字串(character strings)时,常见的一个操作行为就是决定「以 null 为结束符号」的字串的长度。很明显我们不可能写出一份泛型代码取代众所周知原本就存在的极有效率的解法:是的,C 标准函式 strlenwcslen 通常是以组合语言完成,如果再加上适量的硬体支援,就能够比 C++ 泛型版本有明显的速度优势。C++ 标准程式库的作者了解这一点,所以他们抽取出 charwchar_t 的性质,置於 class char_traits 内。於是,泛型代码一旦处理字元字串,便可以简单地使用 char_traits<>::length 来决定一个「以 null 为结束符号」的字串的长度,并且很安心地确知 char_traits 的特化版本将采用最适当的方法来完成任务。

Type traits(型别特性)

Class char_traits 是「把一系列与型别相关的性质包裹於单一 class 之内」的典型例子,那正是 Nathan Myers 所谓的 baggage class [叁考资料1]。在 Boost type-traits library 中,我们 [叁考资料2] 完成了一组非常特别的 traits classes,其中每一个 classes 都封装了 C++ 型别系统中的一个(仅仅一个)特性。所谓特性(trait)指的是,举个例子,某型别是否为一个 pointer,或是一个 reference?某型别是否拥有一个 trivial constructor,或是拥有一个 const 修饰词? 这些 type-traits classes 共同享有一致性的设计:每一个 class 都有一个 member value,那是一个编译期常数,如果某型别拥有某种特性,此一常数的值就是 true,否则就是 false。稍後我将为你展示,这些 classes 可以被使用於泛型编程之中,用来决定某个型别的特性,并导入对应的最佳化措施。

Boost type-traits library 也内含一组 classes,可以针对某个型别执行专属的特定的转换。例如它们可以从某个型别身上移除一个上层的 const 或 volatile。每一个用来执行转换的 class 都定义有一个 typedef-member type,那便是转换结果。所有这些 type-traits classes 都被定义於 namespace boost 之中。为求简化,本文的范例代码大多省略命名空间的设定。

实作(Implementation)

要在这里显示 type-traits library 的所有实作内容,是不可能的,那真是太多太多了。如果你有这个需求,请看 Boost library 的源码。大部份实作方法都是重复的,所以这里我只给你一种风貌,为你示范这些 classes 如何实作出来。让我们从程式库中最简单的一个 class 开始。is_void<T> 有一个 member value,如果 T 是 void,它就是 true。

template <typename T> 
struct is_void
{ static const bool value = false; };

template <> 
struct is_void<void>
{ static const bool value = true; };

在这里,我们定义了 template class is_void 的一个主版本,并针对「T 是 void」的情况提供了一个全特化( full-specialisation)版。虽然 template class 的全特化是一项重要技术,但有时候我们需要的解决方案介於「完全泛化」和「完全特化」之间。这正是标准委员会之所以定义出偏特化(partial template-class specialisation)的原因。举个例子,让我们考虑 class boost::is_pointer<T>,这里我们需要一个主版本,用来处理「T 不为指标」的所有情况,以及一个偏特化版本,用来处理「T 是指标」的情况:

template <typename T> 
struct is_pointer 
{ static const bool value = false; };

template <typename T> 
struct is_pointer<T*> 
{ static const bool value = true; };

偏特化的语法带了点不可思议的味道,而且一谈到它很容易就耗掉一整篇文章。就像全特化的情形一样,为了针对某个 class 写出一个偏特化版本,你首先必须宣告 template 主版本。偏特化版本在 class 名称之後多出一个 <┅> ,其中内含偏特化叁数;这些叁数定义出「将被系结於偏特化版」的某些型别。究竟什麽叁数会(或说能够)出现於偏特化版本之中,规则颇为曲折,以下是一个简略的规则。如果你能够以此型式合法写出两个多载化函式:

void foo(T);
void foo(U);

那麽你就能够以此型式写出一个偏特化版本:

template <typename T>
class c{ /*details*/ };

template <typename T>
class c<U>{ /*details*/ };

这个简则并非绝对成立,但它非常简单,足以让你牢牢记住并足够接近精确的规则。

至於比较复杂的偏特化例子,让我们考虑 class remove_bounds<T>。这个 class 定义了唯一一个 typedef-member type,其型别与 T 相同,但移除任何上层(top level)的 array 边界;这是「traits class 对某个型别进行转换」的例子:

template <typename T> 
struct remove_bounds
{ typedef T type; };

template <typename T, std::size_t N> 
struct remove_bounds<T[N]>
{ typedef T type; };

remove_bounds 的目的是:想像一个泛型演算法,接受一个 array 型别做为 template 叁数,remove_bounds 会提供一个方法,让你有办法得知底部(underlying)的 array 型别。例如,remove_bounds<int[4][5]>::type 会被核定为型别 int[5]。这个例子也向你展示,在一个偏特化版本中,template 叁数的个数并不需要吻合 default template 中的个数。然而,出现於 class 名称之後的叁数个数必须吻合 default template 的叁数个数和叁数型别。

copy 最佳化

现在我要举一个例子,说明我们可以如何运用 type traits classes。考虑标准程式库所提供的 copy 演算法:

template<typename Iter1, typename Iter2>
Iter2 copy(Iter1 first, Iter1 last, Iter2 out);

很明显,写一个泛型版本的 copy 绝无问题,它可以处理任何型别的迭代器 Iter1和 Iter2。然而在某种情况下,copy 动作可以透过 memcpy 完成。为了能够以 memcpy 完成 copy,以下条件必须成立:

  • 两个迭代器 Iter1 和 Iter2 的型别都必须是指标。
  • Iter1 和 Iter2 都必须指向相同的型别 - 但允许有不同的 constvolatile 修饰词。
  • Iter1 所指的型别必须有一个 trivial assignment operator。

所谓 trivial assignment operator,我的意思是这个型别如果不是一个纯量型别(scalar types)[叁考资料3],就是:

  • 这个型别没有使用者自定的 assignment operator。
  • 这个型别没有任何 data members 采用 reference 型式。
  • 所有的 base classes,以及所有的 data member objects 都有 trivial assignment operators。

如果上述所有条件都获得满足,那麽这个型别就可以 memcpy 直接拷贝,而不使用一个由编译器产生的 assignment operator。type-traits library 提供了一个 class has_trivial_assign,使得当 T 有一个 trivial assignment operator 时,has_trivial_assign<T>::value 为 true。这个 class 只能对纯量型别起作用,但你很轻易就可以将它特殊化,使它适用於那些也拥有 trivial assignment operator 的 class/struct。换一个角度说,如果 has_trivial_assign 给出错误的答案,它会导致安全性方面的错误。

列表一显示一个最佳化(使用 memcpy)的 copy 代码。代码之中首先定义一个 template class copier,接受唯一一个 template 叁数 Boolean,然後是一个 static template member function do_copy,执行 copy 的泛型版本(也就是比较慢但比较安全的版本)。接下来是一个 copier<true> 特化版本,其中也定义了一个 static template member function do_copy,这一次使用 memcpy 来执行最佳化拷贝动作。

为了完成整份实作代码,现在我们需要一个 copy 版本;如果可以安全使用 memcpy就让它呼叫 copier<true>::do_copy 执行特化版本,否则就呼叫 copier<false>::do_copy 执行泛化版本。这正是列表一的代码的所作所为。为了了解这些代码如何运作,请看 copy 函式代码,并首先注意最前面的两个 typedefs v1_tv2_t。它们使用 std::iterator_traits<Iter1>::value_type 来得知两个迭代器所指的是什麽型别,然後将其结果喂给另一个 type-traits class remove_cv,用以移除上层的 const- 或 volatile-修饰词,这使 copy 得以比较两个型别而不在乎 const- 或 volatile- 修饰词。接下来,copy 宣告一个列举元 can_opt,它将成为 copier 的 template 叁数 - 在这里,宣告为常数只是为了方便:数值可以被直接传递给 class copier(译注:我无法理解这一段意思;代码本身并未出现常数宣告)can_opt 的值是根据「以下所有项目都验证为真」而计算出来:

  • 首先,两个迭代器必须指向相同型别 - 验证方法是透过 type-traits class is_same
  • 其次,两个迭代器都必须是真正的指标 - 验证方法是透过先前描述过的 class is_pointer
  • 最後,被迭代器所指的型别必须有一个 trivial assignment operator - 验证方法是透过 has_trivial_assign

最後,我们可以使用 can_opt 的值做为 template 引数,传给 copier。这里所呈现的 copy 版本会根据它所获得的叁数而调整,如果有可能使用 memcpy,它就会那麽做,否则就使用一个泛型的 copy。

值得如此吗?

许多文章都会引用这句话:「贸然实施最佳化,是各种伤害的根源」("premature optimization is the root of all evil") [叁考资料4]。所以你一定会问这样的问题:我们的最佳化是否太过贸然?是否太过唐突?为了透视这一点,我把我们的  copy 版本拿来和一个传统的泛型版本做比较 [叁考资料5],结果显示於表一。

很明显,最佳化与否,造成两个截然不同的结果。但我也要持平地说,时间的量测并不含括「快取装置误击效应」(cache miss effects),因此这份结果并未能在两个演算法之间展现精确的比较。然而,或许我们可以加上一些警告,放到「贸然最佳化」的规则里头:

  • 如果你一开始就使用正确的演算法,那麽最佳化就不再有必要。某些情况下,memcpy 是正确的演算法。
  • 如果某个组件即将在许多地方被许多人使用,那麽最佳化是值得的 - 即使对少数使用者而言,最佳化可能是小题大作。
表一:以 copy<const T*, T*> 拷贝1000 个元素,所花费的时间(微秒)

版本

型别 T

时间(微秒)

最佳化的 copychar0.99
传统的 copychar8.07
最佳化的 copyint2.52
传统的 copyint8.02

 

Pair of References

「copy 行为最佳化」这个实例告诉我们,type traits 如何被用来在编译时期执行最佳化策略。type traits 的另一个重要用途是允许某些「除非运用极端的偏特化,否则无法通过编译」的代码得以被顺利编译。只要将偏特化行为授权(delegating)给type traits classes,便有可能做到。关於这种用法,我举的例子是一个可以持有 references 的 pair [叁考资料6]

首先让我们检验 "std::pair" 的定义,为了简化,我略去其中的 comparision operators, default constructor, 和 template copy constructor:

template <typename T1, typename T2> 
struct pair 
{
  typedef T1 first_type;
  typedef T2 second_type;

  T1 first;
  T2 second;

  pair(const T1 & nfirst, const T2 & nsecond)
  :first(nfirst), second(nsecond) { }
};

此刻这个 "pair" 无法持有 references,因为如此一来其 constructor 将被迫接受一个 reference to reference,而这种语法目前并不存在 [叁考资料7]。让我们想想,为了允许 "pair" 得以持有 non-reference 型别、references 型别、constant references 型别,constructor 的叁数必须是什麽样子:

"T1" 的型别constructor 的叁数型别
T
const T &
T &
T &
const T &
const T &

一个和 type traits classes 类似的技术,允许我们建构单一的对应关系,使我们得以根据 contained class 的型别决定叁数型别。type traits classes 提供了一个 "add_reference" 转换,可以为自身型别加上一个 reference,除非它本身已经是一个 reference。

"T1" 的型别"const T1" 的型别"add_reference<const T1>::type" 的型别
T
const T
const T &
T &
T &  [注8]
T &
const T &
const T &
const T &

这使我们得以建立一个 template 主版本,定义一个可内含 non-reference 型别、 reference 型别、constant reference 型别的 "pair" :

template <typename T1, typename T2> 
struct pair 
{
  typedef T1 first_type;
  typedef T2 second_type;

  T1 first;
  T2 second;

  pair(boost::add_reference<const T1>::type nfirst,
       boost::add_reference<const T2>::type nsecond)
  :first(nfirst), second(nsecond) { }
};

为它回添标准的 comparision operators, default constructor 和 template copy constructor 之後(它们都和原先版本相同),你就有了一个可以内含 reference 型别的 std::pair。

当然我们也可以使用偏特化技巧完成同样的扩充,但果真如此,我们需要三个 "pair" 偏特化版本和一个主版本。Type traits 允许我们仅仅定义一个主版本,就可以自动而神奇地将自己调整为任何偏特化版,取代一一偏特化的所谓「暴力法」。以此方式使用 type traits,可允许程式员将偏特化授权(delegate)给 type traits classes,使得代码比较容易维护,也比较容易被理解。

结论

希望这篇文章能够给你一些想法,让你大略知道 type-traits 是什麽。boost 说明文件中有更完整的 classes 列表,以及更进一步的使用范例。Templates 使 C++ 有能力实现泛型编程所带来的复用性;这篇文章还告诉你,templates 可以和 generic 一样地美好。这都有赖 type traits 带来的价值。

致谢

感谢 Beman Dawes 和 Howard Hinnant 对本文所提的意见。

叁考资料
  1. Nathan C. Myers, C++ Report, June 1995.
  2. 这个 type traits library 的完成,要感谢 Steve Cleary, Beman Dawes, Howard Hinnant 和 John Maddock。你可以在 www.boost.org 找到它。
  3. 所谓纯量型别(scalar type)就是算术型别(例如内建的整数或浮点数)、列举型别(enumeration)、指标、函式指标、或以上任何型别再加上 const- 或 volatile- 修饰词。
  4. 此句引自 Donald Knuth, ACM Computing Surveys, December 1974, pg 268.
  5. 这一份测试代码是 boost utility library 的一部份(见 algo_opt_examples.cpp),以 gcc 2.95 编译完成,所有最佳化选项都打开。我的测试结果是在 400MHz Pentium II + Microsoft Windows 98 上获得。
  6. John Maddock 和 Howard Hinnant 已经送出一个 "compressed_pair" library 给 Boost,其中使用的一个技术,和此处所描述的技术类似,也是用来持有 references。他们的 pair 也使用 type traits 来决定是否有任何型别是空的,并且采用 "derive" 而非 "contain" 的方式,用以保存空间 -- 这正是 "compressed" 的命名由来。
  7. 这其实是 C++ 核心语言工作小组的一个议题,由 Bjarne Stroustrup 提出。暂时的解决办法是,允许 "a reference to a reference to T" 的意义等同於 "a reference to T",但是只能存在於 template 具现实体中,或是存在於一个「具备多个 const-volatile 修饰词」的  method 中。
  8. 为什麽这里不该有 const 修饰词呢?对此感到惊讶的人,我要提醒你,请记住, references 永远是个隐晦常数(举个例子,你不能够重新对一个 reference 赋值)。同时也请你记住,"const T &" 是完全不同的东西。因为这些理由,template 型别引数如果本身是个 references 的话,其「const-volatile 修饰词」都被忽略。

 

列表一
namespace detail{

template <bool b>
struct copier
{
   template<typename I1, typename I2>
   static I2 do_copy(I1 first, 
                     I1 last, I2 out);
};

template <bool b>
template<typename I1, typename I2>
I2 copier<b>::do_copy(I1 first, 
                      I1 last, 
                      I2 out)
{
   while(first != last)
   {
      *out = *first;
      ++out;
      ++first;
   }
   return out;
}

template <>
struct copier<true>
{
   template<typename I1, typename I2>
   static I2* do_copy(I1* first, I1* last, I2* out)
   {
      memcpy(out, first, (last-first)*sizeof(I2));
      return out+(last-first);  // 译注:因为是 RandomAccessIterator
   }
};

}

template<typename I1, typename I2>
inline I2 copy(I1 first, I1 last, I2 out)
{
   typedef typename 
    boost::remove_cv<
     typename std::iterator_traits<I1>
      ::value_type>::type v1_t;

   typedef typename 
    boost::remove_cv<
     typename std::iterator_traits<I2>
      ::value_type>::type v2_t;

   enum{ can_opt = 
      boost::is_same<v1_t, v2_t>::value
      && boost::is_pointer<I1>::value
      && boost::is_pointer<I2>::value
      && boost::has_trivial_assign<v1_t>::value 
   };

   return detail::copier<can_opt>::do_copy(first, last, out);
}

浏览数(75) |  评论数(0) | 02-27 14:08
c++的一些名词  

trivial destructor 只无意义的无关痛痒的析构函数,如果大量的多次的调用这种无意义的析构函数对效率是种伤害。

浏览数(30) |  评论数(0) | 02-27 14:05
template  

模板类的偏特化是只可以将另外一个未实例化的模板作为其一个模板类型实参,只有模板类才可以被偏特化,而模板函数是不可以偏特化,会导致编译错误。(partial specialization of function templates )      

模板类中可以嵌套定义模板类和模板函数。

bound friend templates. 模板类的约束模板友元函数,《c++ primer plus》p529 应该是"然后在模板中再次将函数声明为友元",当模板被特化的时候,其友元函数也被相应的特化,举个例子:

template<typename T> void count();//declare function

template<typename T> void report(T &);

 

template<typename T>

class HasFriendT

{

    ...

    friend void count<T>();

    friend void report< > (HasFriendT<T>  &);

    friend bool operator == <T> (const HasFriendT<T>&);

}

 

特化的结果是:

HasFriendT<int> t;

编译器所做的事情是:

class HasFriendT<int>

{

    ...

    friend void count <int> ();

    friend void report <> (HasFriendT<int>  &);   //相当于 friend void report <HasFriendT<int> >(HasFriendT<int> &);这里要切记了

}

 

浏览数(29) |  评论数(0) | 02-27 11:32
启用内存泄漏检测  

C/C++ 编程语言的最强大功能之一便是其动态分配和释放内存,但是中国有句古话:“最大的长处也可能成为最大的弱点”,那么 C/C++ 应用程序正好印证了这句话。在 C/C++ 应用程序开发过程中,动态分配的内存处理不当是最常见的问题。其中,最难捉摸也最难检测的错误之一就是内存泄漏,即未能正确释放以前分配的内存的错误。偶尔发生的少量内存泄漏可能不会引起我们的注意,但泄漏大量内存的程序或泄漏日益增多的程序可能会表现出各种各样的征兆:从性能不良(并且逐渐降低)到内存完全耗尽。更糟的是,泄漏的程序可能会用掉太多内存,导致另外一个程序垮掉,而使用户无从查找问题的真正根源。此外,即使无害的内存泄漏也可能殃及池鱼。

  幸运的是,Visual Studio 调试器和 C 运行时 (CRT) 库为我们提供了检测和识别内存泄漏的有效方法。下面请和我一起分享收获——如何使用 CRT 调试功能来检测内存泄漏?

一、如何启用内存泄漏检测机制

  VC++ IDE 的默认状态是没有启用内存泄漏检测机制的,也就是说即使某段代码有内存泄漏,调试会话的 Output 窗口的 Debug 页不会输出有关内存泄漏信息。你必须设定两个最基本的机关来启用内存泄漏检测机制。

  一是使用调试堆函数:


#define _CRTDBG_MAP_ALLOC
#include<stdlib.h>

#include<crtdbg.h>

  注意:#include 语句的顺序。如果更改此顺序,所使用的函数可能无法正确工作。

  通过包含 crtdbg.h 头文件,可以将 malloc 和 free 函数映射到其“调试”版本 _malloc_dbg 和 _free_dbg,这些函数会跟踪内存分配和释放。此映射只在调试(Debug)版本(也就是要定义 _DEBUG)中有效。发行版本(Release)使用普通的 malloc 和 free 函数。#define 语句将 CRT 堆函数的基础版本映射到对应的“调试”版本。该语句不是必须的,但如果没有该语句,那么有关内存泄漏的信息会不全。

  二是在需要检测内存泄漏的地方添加下面这条语句来输出内存泄漏信息:

_CrtDumpMemoryLeaks();

  当在调试器下运行程序时,_CrtDumpMemoryLeaks 将在 Output 窗口的 Debug 页中显示内存泄漏信息。比如: Detected memory leaks!

Dumping objects ->

C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.

Data: <AB> 41 42

c:/program files/microsoft visual studio/vc98/include/crtdbg.h(552) : {44} normal
block at 0x00441BD0, 33 bytes long.

Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD

c:/program files/microsoft visual studio/vc98/include/crtdbg.h(552) : {43} normal
block at 0x00441C20, 40 bytes long.

Data: < C > 08 02 43 00 16 00 00 00 00 00 00 00 00 00 00 00

Object dump complete.


  如果不使用 #define _CRTDBG_MAP_ALLOC 语句,内存泄漏的输出是这样的:
Detected memory leaks!

Dumping objects ->

{45} normal block at 0x00441BA0, 2 bytes long.
Data: <AB> 41 42

{44} normal block at 0x00441BD0, 33 bytes long.
Data: < C > 00 43 00 CD CD CD CD CD CD CD CD CD CD CD CD CD

{43} normal block at 0x00441C20, 40 bytes long.
Data: < C > C0 01 43 00 16 00 00 00 00 00 00 00 00 00 00 00

Object dump complete.


  根据这段输出信息,你无法知道在哪个源程序文件里发生了内存泄漏。下面我们来研究一下输出信息的格式。第一行和第二行没有什么可说的,从第三行开始:
xx}:花括弧内的数字是内存分配序号,本文例子中是 {45},{44},{43};
block:内存块的类型,常用的有三种:normal(普通)、client(客户端)或 CRT(运行时);本文例子中是:normal block;
用十六进制格式表示的内存位置,如:at 0x00441BA0 等;
以字节为单位表示的内存块的大小,如:32 bytes long;
前 16 字节的内容(也是用十六进制格式表示),如:Data: 41 42 等;

  仔细观察不难发现,如果定义了 _CRTDBG_MAP_ALLOC ,那么在内存分配序号前面还会显示在其中分配泄漏内存的文件名,以及文件名后括号中的数字表示发生泄漏的代码行号,比如:
C:/Temp/memleak/memleak.cpp(15)

  双击 Output 窗口中此文件名所在的输出行,便可跳到源程序文件分配该内存的代码行(也可以选中该行,然后按 F4,效果一样) ,这样一来我们就很容易定位内存泄漏是在哪里发生的了,因此,_CRTDBG_MAP_ALLOC 的作用显而易见。

使用 _CrtSetDbgFlag
  如果程序只有一个出口,那么调用 _CrtDumpMemoryLeaks 的位置是很容易选择的。但是,如果程序可能会在多个地方退出该怎么办呢?在每一个可能的出口处调用 _CrtDumpMemoryLeaks 肯定是不可取的,那么这时可以在程序开始处包含下面的调用:_CrtSetDbgFlag ( _CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF );这条语句无论程序在什么地方退出都会自动调用 _CrtDumpMemoryLeaks。注意:这里必须同时设置两个位域标志:_CRTDBG_ALLOC_MEM_DF 和 _CRTDBG_LEAK_CHECK_DF。

设置 CRT 报告模式
  默认情况下,_CrtDumpMemoryLeaks 将内存泄漏信息 dump 到 Output 窗口的 Debug 页,如果你想将这个输出定向到别的地方,可以使用 _CrtSetReportMode 进行重置。如果你使用某个库,它可能将输出定向到另一位置。此时,只要使用以下语句将输出位置设回 Output 窗口即可:


_CrtSetReportMode( _CRT_ERROR, _CRTDBG_MODE_DEBUG );
  有关使用 _CrtSetReportMode 的详细信息,请参考 MSDN 库关于 _CrtSetReportMode 的描述。

二、解释内存块类型

  前面已经说过,内存泄漏报告中把每一块泄漏的内存分为 normal(普通块)、client(客户端块)和 CRT 块。事实上,需要留心和注意的也就是 normal 和 client,即普通块和客户端块。
  1.normal block(普通块):这是由你的程序分配的内存。
  2.client block(客户块):这是一种特殊类型的内存块,专门用于 MFC 程序中需要析构函数的对象。MFC new 操作符视具体情况既可以为所创建的对象建立普通块,也可以为之建立客户块。
  3.CRT block(CRT 块):是由 C RunTime Library 供自己使用而分配的内存块。由 CRT 库自己来管理这些内存的分配与释放,我们一般不会在内存泄漏报告中发现 CRT 内存泄漏,除非程序发生了严重的错误(例如 CRT 库崩溃)。

  除了上述的类型外,还有下面这两种类型的内存块,它们不会出现在内存泄漏报告中:
  1.free block(空闲块):已经被释放(free)的内存块。
  2.Ignore block(忽略块):这是程序员显式声明过不要在内存泄漏报告中出现的内存块。

三、如何在内存分配序号处设置断点

  在内存泄漏报告中,的文件名和行号可告诉分配泄漏的内存的代码位置,但仅仅依赖这些信息来了解完整的泄漏原因是不够的。因为一个程序在运行时,一段分配内存的代码可能会被调用很多次,只要有一次调用后没有释放内存就会导致内存泄漏。为了确定是哪些内存没有被释放,不仅要知道泄漏的内存是在哪里分配的,还要知道泄漏产生的条件。这时内存分配序号就显得特别有用——这个序号就是文件名和行号之后的花括弧里的那个数字。

  例如,在本文例子代码的输出信息中,“45”是内存分配序号,意思是泄漏的内存是你程序中分配的第四十五个内存块:


Detected memory leaks!

Dumping objects ->

C:/Temp/memleak/memleak.cpp(15) : {45} normal block at 0x00441BA0, 2 bytes long.

Data: <AB> 41 42

......

Object dump complete.

  CRT 库对程序运行期间分配的所有内存块进行计数,包括由 CRT 库自己分配的内存和其它库(如 MFC)分配的内存。因此,分配序号为 N 的对象即为程序中分配的第 N 个对象,但不一定是代码分配的第 N 个对象。(大多数情况下并非如此。)这样的话,你便可以利用分配序号在分配内存的位置设置一个断点。方法是在程序起始附近设置一个位置断点。当程序在该点中断时,可以从 QuickWatch(快速监视)对话框或 Watch(监视)窗口设置一个内存分配断点:

  例如,在 Watch 窗口中,在 Name 栏键入下面的表达式:


_crtBreakAlloc

  如果要使用 CRT 库的多线程 DLL 版本(/MD 选项),那么必须包含上下文操作符,像这样:
{,,msvcrtd.dll}_crtBreakAlloc

  现在按下回车键,调试器将计算该值并把结果放入 Value 栏。如果没有在内存分配点设置任何断点,该值将为 –1。

  用你想要在其位置中断的内存分配的分配序号替换 Value 栏中的值。例如输入 45。这样就会在分配序号为 45 的地方中断。

  在所感兴趣的内存分配处设置断点后,可以继续调试。这时,运行程序时一定要小心,要保证内存块分配的顺序不会改变。当程序在指定的内存分配处中断时,可以查看 Call Stack(调用堆栈)窗口和其它调试器信息以确定分配内存时的情况。如果必要,可以从该点继续执行程序,以查看对象发生了什么情况,或许可以确定未正确释放对象的原因。

  尽管通常在调试器中设置内存分配断点更方便,但如果愿意,也可在代码中设置这些断点。为了在代码中设置一个内存分配断点,可以增加这样一行(对于第四十五个内存分配):


_crtBreakAlloc = 45;

  你还可以使用有相同效果的 _CrtSetBreakAlloc 函数:
_CrtSetBreakAlloc(45);


四、如何比较内存状态

  定位内存泄漏的另一个方法就是在关键点获取应用程序内存状态的快照。CRT 库提供了一个结构类型 _CrtMemState。你可以用它来存储内存状态的快照:
_CrtMemState s1, s2, s3;

  若要获取给定点的内存状态快照,可以向 _CrtMemCheckpoint 函数传递一个 _CrtMemState 结构。该函数用当前内存状态的快照填充此结构:
_CrtMemCheckpoint( &s1 );

  通过向 _CrtMemDumpStatistics 函数传递 _CrtMemState 结构,可以在任意地方 dump 该结构的内容:
_CrtMemDumpStatistics( &s1 );

  该函数输出如下格式的 dump 内存分配信息:
0 bytes in 0 Free Blocks.
75 bytes in 3 Normal Blocks.
5037 bytes in 41 CRT Blocks.
0 bytes in 0 Ignore Blocks.
0 bytes in 0 Client Blocks.
Largest number used: 5308 bytes.
Total allocations: 7559 bytes.

  若要确定某段代码中是否发生了内存泄漏,可以通过获取该段代码之前和之后的内存状态快照,然后使用 _CrtMemDifference 比较这两个状态:
_CrtMemCheckpoint( &s1 );// 获取第一个内存状态快照

// 在这里进行内存分配

_CrtMemCheckpoint( &s2 );// 获取第二个内存状态快照

// 比较两个内存快照的差异
if ( _CrtMemDifference( &s3, &s1, &s2) )
_CrtMemDumpStatistics( &s3 );// dump 差异结果

  顾名思义,_CrtMemDifference 比较两个内存状态(前两个参数),生成这两个状态之间差异的结果(第三个参数)。在程序的开始和结尾放置 _CrtMemCheckpoint 调用,并使用 _CrtMemDifference 比较结果,是检查内存泄漏的另一种方法。如果检测到泄漏,则可以使用 _CrtMemCheckpoint 调用通过二进制搜索技术来分割程序和定位泄漏。

五、结论

  尽管 VC ++ 具有一套专门调试 MFC 应用程序的机制,但本文上述讨论的内存分配很简单,没有涉及到 MFC 对象,所以这些内容同样也适用于 MFC 程序。在 MSDN 库中可以找到很多有关 VC++ 调试方面的资料,如果你能善用 MSDN 库,相信用不了多少时间你就有可能成为调试高手。

浏览数(37) |  评论数(0) | 02-27 10:41
[代码]内存池操作的模板类  

// 文件名 : Mempool.h
// 作者 : Moonwell
// Msn : archonzhao@hotmail.com
// 将#include "mempool.h"放在#include <windows.h>之前
// 否则会出现"InitializeCriticalSectionAndSpinCount函数没定义"的错误
#ifndef _MEM_POOL_H
#define _MEM_POOL_H
#define _WIN32_WINNT 0x0403 //For InitializeCriticalSectionAndSpinCount
#ifdef _DEBUG
#define _DEBUG_POOL
#endif
#include <windows.h>
#include <crtdbg.h>
#include <vector>
#ifdef _DEBUG_POOL
#include <map>
#endif
using namespace std;
template<typename memtype>
class MemPool
{
public:
//init_count = 初始分配的对象个数
MemPool(int init_count);

~MemPool(void);
memtype *alloc(); // 读取一个元素
void free(memtype *obj);// 删除一个元素
private:
vector<memtype *> m_free_node; //空闲的对象列表
vector<memtype *> m_new_node; //额外创建的对象列表
#ifdef _DEBUG_POOL
map<memtype *,memtype *> m_map;//用于检测对象是否被释放了多次
#endif
memtype *m_head; //初始创建时候的内存块的指针
CRITICAL_SECTION m_sec;
};
template<typename memtype>
MemPool<memtype>::MemPool(int init_count)
{
m_head = new memtype[init_count];
//先把vector对象的内存增加到原来的两倍大小,可以提高效率
m_free_node.resize(init_count*2);
m_free_node.resize(0);
//将对象添加到空闲列表
for(int i=0;i<init_count;i++)
m_free_node.push_back(&m_head[i]);
InitializeCriticalSectionAndSpinCount(&m_sec,64);
}
template<typename memtype>
MemPool<memtype>::~MemPool(void)
{
vector<memtype *>::iterator it;
DeleteCriticalSection(&m_sec);
delete [] m_head;

// 删除所有额外创建的对象
for(it = m_new_node.begin();it != m_new_node.end();++it)
delete [] *it;
}
template<typename memtype>
memtype *MemPool<memtype>::alloc()
{
memtype *ptmp;
EnterCriticalSection(&m_sec);
if(!m_free_node.empty()) {
ptmp = *(--m_free_node.end()); //取最后一个对象
m_free_node.pop_back();
#ifdef _DEBUG_POOL
m_map.erase(ptmp);
#endif
} else {
ptmp = new memtype; //创建额外的对象
m_new_node.push_back(ptmp); //添加到额外列表
}
LeaveCriticalSection(&m_sec);
return ptmp;
}
template<typename memtype>
void MemPool<memtype>::free(memtype *obj)
{
EnterCriticalSection(&m_sec);
m_free_node.push_back(obj);
#ifdef _DEBUG_POOL //obj已经被释放了吗?
_ASSERTE(m_map.find(obj) == m_map.end());
m_map[obj] = obj;
#endif
LeaveCriticalSection(&m_sec);
}
#endif

 

在原来基础上增加了多次free()检测的功能,对同一个对象释放两次将会引发一个断言.

 

浏览数(22) |  评论数(0) | 02-27 00:43
VC++多线程下内存操作的优化  

许多程序员发现用VC++编写的程序在多处理器的电脑上运行会变得很慢,这种情况多是由于多个线程争用同一个资源引起的。对于用VC++编写的程序,问题出在VC++的内存管理的具体实现上。以下通过对这个问题的解释,提供一个简便的解决方法,使得这种程序在多处理器下避免出现运行瓶颈。这种方法在没有VC++程序的源代码时也能用。

问题

    C和C++运行库提供了对于堆内存进行管理的函数:C提供的是malloc()和free()、C++提供的是new和delete。无论是通过malloc()还是new申请内存,这些函数都是在堆内存中寻找一个未用的块,并且块的大小要大于所申请的大小。如果没有足够大的未用的内存块,运行时间库就会向操作系统请求新的页。页是虚拟内存管理器进行操作的单位,在基于Intel的处理器的NT平台下,一般是4,096字节。当你调用free()或delete释放内存时,这些内存块就返还给堆,供以后申请内存时用。


    这些操作看起来不太起眼,但是问题的关键。问题就发生在当多个线程几乎同申请内存时,这通常发生在多处理器的系统上。但即使在一个单处理器的系统上,如果线程在错误的时间被调度,也可能发生这个问题。

考虑处于同一进程中的两个线程,线程1在申请1,024字节的内存的同时,运行于另外一个处理器的线程2申请256字节内存。内存管理器发现一个未用的内存块用于线程1,同时同一个函数发现了同一块内存用于线程2。如果两个线程同时更新内部数据结构,记录所申请的内存及其大小,堆内存就会产生冲突。即使申请内存的函数者成功返回,两个线程都确信自己拥有那块内存,这个程序也会产生错误,这只是个时间问题。

产生这种情况称为争用,是编写多线程程序的最大问题。解决这个问题的关键是要用一个锁定机制来保护内存管理器的这些函数,锁定机制保证运行相同代码的多个线程互斥地进行,如果一个线程正运行受保护的代码,则其他的线程都必须等待,这种解决方法也称作序列化。

    NT提供了一些锁定机制的实现方法。CreateMutex()创建一个系统范围的锁定对象,但这种方法的效率最低;InitializeCriticalSection()创建的critical section相对效率就要高许多;要得到更好的性能,可以用具有service pack 3的NT 4的spin lock,更详细的信息可以参考VC++帮助中的InitializeCriticalSectionAndSpinCount()函数的说明。有趣的是,虽然帮助文件中说spin lock用于NT的堆管理器(HeapAlloc()系列的函数),VC++运行库的堆管理函数并没有用spin lock来同步对堆的存取。如果查看VC++运行库的堆管理函数的源程序,会发现是用一个critical section用于全部的内存操作。如果可以在VC++运行库中用HeapAlloc(),而不是其自己的堆管理函数,将会因为使用的是spin lock而不是critical section而得到速度优化。

通过使用critical section同步对堆的存取,VC++运行库可以安全地让多个线程申请和释放内存。然而,由于内存的争用,这种方法会引起性能的下降。如果一个线程存取另外一个线程正在使用的堆时,前一个线程就需要等待,并丧失自己的时间片,切换到其他的线程。线程的切换在NT下是相当费时的,因为其占用线程的时间片的一个小的百分比。如果有多个线程同时要存取同一个堆,会引起更多的线程切换,足够引起极大的性能损失。

 

现象

    如何发现多处理器系统存在这种性能损失?有一个简便的方法,打开“管理工具”中的“性能”监视器,在系统组中添加一个上下文切换/秒计数,然后运行想要测试的多线程程序,并且在进程组中添加该进程的处理器时间计数,这样就可以得到处理器在高负荷下要发生多少次上下文切换。

在高负荷下有上千次的上下文切换是正常的,但当计数超过80,000或100,000时,说明过多的时间都浪费在线程的切换,稍微计算一下就可以知道,如果每秒有100,000次线程切换,则每个线程只有10微秒用于运行,而NT上的正常的时间片长度约有12毫秒,是前者的上千倍。

    图1的性能图显示了过度的线程切换,而图2显示了同一个进程在同样的环境下,在使用了下面提供的解决方法后的情况。图1的情况下,系统每秒钟要进行120,000次线程切换,改进后,每秒钟线程切换的次数减少到1,000次以下。两张图都是在运行同一个测试程序时截取得,程序中同时有3个线程同时进行最大为2,048字节的堆的申请,硬件平台是一个双Pentium II 450机器,有256MB内存。

 

解决方法

本方法要求多线程程序是用VC++编写的,并且是动态链接到C运行库的。要求NT系统所安装的VC++运行库文件msvcrt.dll的版本号是6,所安装的service pack的版本是5以上。如果程序是用VC++ v6.0以上版本编译的,即使多线程程序和libcmt.lib是静态链接,本方法也可以使用。

    当一个VC++程序运行时,C运行库被初始化,其中一项工作是确定要使用的堆管理器,VC++ v6.0运行库既可以使用其自己内部的堆管理函数,也可以直接调用操作系统的堆管理函数(HeapAlloc()系列的函数),在__heap_select()函数内部分执行以下三个步骤:

    1、检查操作系统的版本,如果运行于NT,并且主版本是5或更高(Window 2000及以后版本),就使用HeapAlloc()。

    2、查找环境变量__MSVCRT_HEAP_SELECT,如果有,将确定使用哪个堆函数。如果其值是__GLOBAL_HEAP_SELECTED,则会改变所有程序的行为。如果是一个可执行文件的完整路径,还要调用GetModuleFileName()检查是否该程序存在,至于要选择哪个堆函数还要查看逗号后面的值,1表示使用HeapAlloc(),2表示使用VC++ v5的堆函数,3表示使用VC++ v6的堆函数。

    3、检测可执行文件中的链接程序标志,如果是由VC++ v6或更高的版本创建的,就使用版本6的堆函数,否则使用版本5的堆函数。

    那么如何提高程序的性能?如果是和msvcrt.dll动态链接的,保证这个dll是1999年2月以后,并且安装的service pack的版本是5或更高。如果是静态链接的,保证链接程序的版本号是6或更高,可以用quickview.exe程序检查这个版本号。要改变所要运行的程序的堆函数的选取,在命令行下键入以下命令:

set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1

    以后,所有从这个命令行运行的程序,都会继承这个环境变量的设置。这样,在堆操作时都会使用HeapAlloc()。如果让所有的程序都使用这些速度更快的堆操作函数,运行控制面板的“系统”程序,选择“环境”,点取“系统变量”,输入变量名和值,然后按“应用”按钮关闭对话框,重新启动机器。

按照微软的说法,可能有一些用VC++ v6以前版本编译程序,使用VC++ v6的堆管理器会出现一些问题。如果在进行以上设置后遇到这样的问题,可以用一个批处理文件专门为这个程序把这个设置去掉,例如:

set __MSVCRT_HEAP_SELECT=c:/program files/myapp/myapp.exe,1 c:/bin/buggyapp.exe,2

 

测试

    为了验证在多处理器下的效果,编了一个测试程序heaptest.c。该程序接收三个参数,第一个参数表示线程数,第二个参数是所申请的内存的最大值,第三个参数每个线程申请内存的次数。

#define WIN32_LEAN_AND_MEAN

#include <windows.h>

#include <process.h>

#include <stdio.h>

#include <stdlib.h>

/* compile with cl /MT heaptest.c */

/* to switch to the system heap issue the following command

   before starting heaptest from the same command line

   set __MSVCRT_HEAP_SELECT=__GLOBAL_HEAP_SELECTED,1 */

/* structure transfers variables to the worker threads */

typedef struct tData

{

    int maximumLength;

    int allocCount;

} threadData;

void printUsage(char** argv)

{

    fprintf(stderr,"Wrong number of parameters./nUsage:/n");

    fprintf(stderr,"%s threadCount maxAllocLength allocCount/n/n",

        argv[0]);

    exit(1);

}

unsigned __stdcall workerThread(void* myThreadData)

{

    int count;

    threadData* myData;

    char* dummy;

    srand(GetTickCount()*GetCurrentThreadId());

    myData=(threadData*)myThreadData;

    /* now let us do the real work */

    for(count=0;count<myData->allocCount;count++)

    {

        dummy=(char*)malloc((rand()%myData->maximumLength)+1);

        free(dummy);

    }

    _endthreadex(0);

    /* to satisfy compiler */

    return 0;

}

int main(int argc,char** argv)

{

    int threadCount;

    int count;

    threadData actData;

    HANDLE* threadHandles;

    DWORD startTime;

    DWORD stopTime;

    DWORD retValue;

    unsigned dummy;

    /* check parameters */

    if(argc<4 || argc>4)

        printUsage(argv);

    /* get parameters for this run */

    threadCount=atoi(argv[1]);

    if(threadCount>64)

        threadCount=64;

    actData.maximumLength=atoi(argv[2])-1;

    actData.allocCount=atoi(argv[3]);

    threadHandles=(HANDLE*)malloc(threadCount*sizeof(HANDLE));

    printf("Test run with %d simultaneous threads:/n",threadCount);

    startTime=GetTickCount();

    for(count=0;count<threadCount;count++)

    {

        threadHandles[count]=(HANDLE)_beginthreadex(0,0,

            &workerThread, (void*)&actData,0,&dummy);

        if(threadHandles[count]==(HANDLE)-1)

        {

            fprintf(stderr,"Error starting worker threads./n");

            exit(2);

        }

    }

    /* wait until all threads are done */

    retValue=WaitForMultipleObjects(threadCount,threadHandles

        ,1,INFINITE);

    stopTime=GetTickCount();

    printf("Total time elapsed was: %d milliseconds",

        stopTime-startTime);

    printf(" for %d alloc operations./n",

        actData.allocCount*threadCount);

    /* cleanup */

    for(count=0;count<threadCount;count++)

        CloseHandle(threadHandles[count]);

    free(threadHandles);

    return 0;

}

测试程序在处理完参数后,创建参数1指定数量的线程,threadData结构用于传递计数变量。workThread中进行内存操作,首先初始化随机数发生器,然后进行指定数量的malloc()和free()操作。主线程调用WaitForMultipleObject()等待工作者线程结束,然后输出线程运行的时间。计时不是十分精确,但影响不大。

    为了编译这个程序,需要已经安装VC++ v6.0程序,打开一个命令行窗口,键入以下命令:

cl /MT heaptest.c

    /MT表示同C运行库的多线程版静态链接。如果要动态链接,用/MD。如果VC++是v5.0的话并且有高版本的msvcrt.dll,应该用动态链接。现在运行这个程序,用性能监视器查看线程切换的次数,然后按上面设置环境参数,重新运行这个程序,再次查看线程切换次数。

    当截取这两张图时,测试程序用了60,953ms进行了3,000,000次的内存申请操作,使用的是VC++ v6的堆操作函数。在转换使用HeapAlloc()后,同样的操作仅用了5,291ms。在这个特定的情况下,使用HeapAlloc()使得性能提高了10倍以上!在实际的程序同样可以看到这种性能的提升。

   

结论

多处理器系统可以自然提升程序的性能,但如果发生多个处理器争用同一个资源,则可能多处理器的系统的性能还不如单处理器系统。对于C/C++程序,问题通常发生在当多个线程进行频繁的内存操作活动时。如上文所述,只要进行很少的一些设置,就可能极大地提高多线程程序在多处理器下的性能。这种方法即不需要源程序,也不需要重新编译可执行文件,而最大的好处是用这种方法得到的性能的提高是不用支付任何费用的。

浏览数(15) |  评论数(0) | 02-27 00:43
VC6.0中如何让new操作失败后抛出异常  

标准C++规定new一个对象时如果分配内存失败就应抛出一个std::bad_alloc异常,如果不希望抛出异常而仅仅传回一个NULL指针,可以用new的无异常版本:new(nothrow)。

  VC6.0在<new>头文件中声明了这两种operator new操作符:

void *__cdecl operator new(size_t) _THROW1(std::bad_alloc);
void *__cdecl operator new(size_t, const std::nothrow_t&) _THROW0();


  并分别定义在newop.cpp和newop2.cpp中。而_THROW0和_THROW1则是两个宏,在Include目录的xstddef文件中定义:

#define _THROW0() throw ()
#define _THROW1(x) throw (x)


  newop.cpp和newop2.cpp对应的目标模块被打包进标准C++库中。标准C++库有若干个版本: libcp.lib(单线程静态版)、libcpd.lib(单线程静态调试版)、libcpmt.lib(多线程静态版)、libcpmtd.lib(多线程静态调试版)、msvcprt.lib(多线程动态版的导入库),msvcprtd.lib(多线程动态调试版的导入库),这些库与相应版本的C标准库一起使用,比如libcp.lib与libc.lib搭配。另外,VC6.0在new.cpp还定义了一个operator new,原型如下 :

void * operator new( unsigned int cb )


  而new.cpp对应的目标模块却是被打包进C标准库中的(是不是有点奇怪?)。

  一般来说,程序员不会显式指定链接C++标准库,可是当程序中确实使用了标准C++库时链接器却能聪明地把相应的C++标准库文件加进输入文件列表,这是为什么?其实任何一个C++标准头文件都会直接或间接地包含use_ansi.h文件,打开它一看便什么都清楚了(源码之前,了无秘密) :

/***
*use_ansi.h - pragmas for ANSI Standard C++ libraries
*
* Copyright (c) 1996-1997, Microsoft Corporation. All rights reserved.
*
*Purpose:
* This header is intended to force the use of the appropriate ANSI
* Standard C++ libraries whenever it is included.
*
* [Public]
*
****/


#if _MSC_VER > 1000
#pragma once
#endif

#ifndef _USE_ANSI_CPP
#define _USE_ANSI_CPP

#ifdef _MT
#ifdef _DLL
#ifdef _DEBUG
#pragma comment(lib,"msvcprtd")
#else // _DEBUG
#pragma comment(lib,"msvcprt")
#endif // _DEBUG

#else // _DLL
#ifdef _DEBUG
#pragma comment(lib,"libcpmtd")
#else // _DEBUG
#pragma comment(lib,"libcpmt")
#endif // _DEBUG
#endif // _DLL

#else // _MT
#ifdef _DEBUG
#pragma comment(lib,"libcpd")
#else // _DEBUG
#pragma comment(lib,"libcp")
#endif // _DEBUG
#endif

#endif // _USE_ANSI_CPP


  现在我们用实际代码来测试一下new会不会抛出异常,建一个test.cpp源文件:

// test.cpp
#include <new>
#include <iostream>

using namespace std;

class BigClass
{
 public:
  BigClass() {}
  ~BigClass(){}
  char BigArray[0x7FFFFFFF];
};

int main()
{
 try
 {
  BigClass *p = new BigClass;
 }
 catch( bad_alloc &a)
 {
  cout << "new BigClass, threw a bad_alloc exception" << endl;
 }

 BigClass *q = new(nothrow) BigClass;
 if ( q == NULL )
  cout << "new(nothrow) BigClass, returned a NULL pointer" << endl;
  try
  {
   BigClass *r = new BigClass[1];
  }
  catch( bad_alloc &a)
  {
   cout << "new BigClass[1], threw a bad_alloc exception" << endl;
  }
 return 0;
}


  根据VC6.0编译器与链接器的做法(请参考《为什么会出现LNK2005"符号已定义"的链接错误?》),链接器会首先在C++标准库中解析符号,然后才是C标准库,所以如果开发者没有自定义operator new的话最后程序链接的应该是C++标准库中newop.obj和newop2.obj模块里的代码。可是程序运行的结果却是:

new(nothrow) BigClass, returned a NULL pointer


  显然程序始终未抛出bad_alloc异常。单步跟踪观察,发现第1个和第3个new实际上调用了new.cpp里的operator new,而第二个new(nothrow)则正确地调用了newop2.cpp定义的版本。很难理解是吧?但是当你用

dumpbin /SYMBOLS libcp.lib


  dump出libcp.lib所有的符号信息时,你会发现其中的newop.obj模块没有定义任何符号(其它版本也一样)。不可思议!newop.cpp的实现代码明明写在那儿,怎么会....?让我们再仔细看看newop.cpp,咦,operator new的定义被包裹在一个#if...#endif块中:

#if !defined(_MSC_EXTENSIONS)

...
...

void *__cdecl operator new(size_t size) _THROW1(_STD bad_alloc)


#endif


  原来需要_MSC_EXTENSIONS宏未定义,实现代码才是有效的啊。那么这个宏是什么意思?其实Visual C++在语言层面上对ANSI C标准做了一些特殊的扩展,定义_MSC_EXTENSIONS意味着编译器支持这样的扩展,没有定义它编译器就会严格按照ANSI C标准来编译程序。实际上如果指定了编译选项/Ze编译器就会自动定义这个宏,指定/Za则不会,而且/Ze是缺省选项。作者猜想Visual Studio的开发人员在build标准C++库时很可能没有指定/Za,导致newop.cpp中的operator new定义被无情抛弃。是他们的疏漏吗?我看未必,大家可以试试用/Za选项去编译那些标准库文件,看看有多少编译不通过。VC标准库的实现用了很多微软扩展的语言特性,不指定/Za是情有可原的,我不明白的是newop.cpp的作者(好象是P.J. Plauger老人家)为什么会加上一个如此愚蠢的"#if !defined(_MSC_EXTENSIONS)",因为实在看不出这个operator new定义与_MSC_EXTENSIONS有什么冲突的地方。

  既然标准C++库里的newop.obj是个空壳,那我们就只好自己动手丰衣足食了。把newop.cpp和dbgint.h(都在VC98crtsrc目录下)拷贝到test.cpp所在的目录,并将newop.cpp中的

#include <dbgint.h>


  改成

#include "dbgint.h"


  然后用

cl /c /Za /D_CRTBLD newop.cpp


  编译它。/D_CRTBLD定义了_CRTBLD宏,为什么这么做呢?因为dbgint.h属于内部头文件,VC不希望应用程序用到它,便在文件中埋伏了这么一段:

#ifndef _CRTBLD
/*
* This is an internal C runtime header file. It is used when building
* the C runtimes only. It is not to be used as a public header file.
*/
#error ERROR: Use of C runtime library internal header file.
#endif /* _CRTBLD */


  可我们确确实实是想build标准库(的一部分),所以只好强行突破这个限制了。然后编译test.cpp:

cl /c /GX test.cpp


  最后进行链接:

link test.obj newop.obj


  这时再运行test.exe输出的结果就是

new BigClass, threw a bad_alloc exception
new(nothrow) BigClass, returned a NULL pointer
new BigClass[1], threw a bad_alloc exception


  值得庆幸的是虽然VC6.0如此弱智,但VC7.1却表现良好,原因是VC7.1的newop.cpp和newaop.cpp(数组版)取消了那个愚的"#if !defined(_MSC_EXTENSIONS)",于是标准C++库中的newop.obj和newaop.obj模块都实实在在地有了相应代码。另外,nothrow版的定义也分别转移到了newopnt.cpp和newaopnt.cpp中。

 

  后记: 作者在2001年便碰到过这个问题,百思不得其解,于是在论坛上发问,也不见答复。从此便搁置一旁,直到最近因探究LNK2005链接错误而彻底弄清楚VC链接器解析符号的规则后,才意识到二者或有联系。于是重拾旧疑,顺藤而上,果然问题就迎刃而解。此题虽小,功夫却做足,最后总算水落石出,解除了4年的积惑。

浏览数(30) |  评论数(0) | 02-26 14:18

<script type=text/javascript>var gaJsHost = (("https:" == document.location.protocol) ? "https://ssl." : "http://www.");document.write(unescape("%3Cscript src='" + gaJsHost + "google-analytics.com/ga.js' type='text/javascript'%3E%3C/script%3E"));</script> <script src="http://www.google-analytics.com/ga.js" type=text/javascript></script> <script type=text/javascript>var pageTracker = _gat._getTracker("UA-179165-1");pageTracker._trackPageview();</script>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
提供的源码资源涵盖了安卓应用、小程序、Python应用和Java应用等多个领域,每个领域都包含了丰富的实例和项目。这些源码都是基于各自平台的最新技术和标准编写,确保了在对应环境下能够无缝运行。同时,源码中配备了详细的注释和文档,帮助用户快速理解代码结构和实现逻辑。 适用人群: 这些源码资源特别适合大学生群体。无论你是计算机相关专业的学生,还是对其他领域编程感兴趣的学生,这些资源都能为你提供宝贵的学习和实践机会。通过学习和运行这些源码,你可以掌握各平台开发的基础知识,提升编程能力和项目实战经验。 使用场景及目标: 在学习阶段,你可以利用这些源码资源进行课程实践、课外项目或毕业设计。通过分析和运行源码,你将深入了解各平台开发的技术细节和最佳实践,逐步培养起自己的项目开发和问题解决能力。此外,在求职或创业过程中,具备跨平台开发能力的大学生将更具竞争力。 其他说明: 为了确保源码资源的可运行性和易用性,特别注意了以下几点:首先,每份源码都提供了详细的运行环境和依赖说明,确保用户能够轻松搭建起开发环境;其次,源码中的注释和文档都非常完善,方便用户快速上手和理解代码;最后,我会定期更新这些源码资源,以适应各平台技术的最新发展和市场需求。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值