Archive forD Language

D语言的GC

从目前掌握的资料来看,似乎只有D语言规范里稍稍提了一下D语言的内存模型,并没有深入描写.以下细节来自D 2.0语言运行时的实现代码,若以后运行时实现有所变更,请参考最新D运行时实现(所有代码均参考DMD的实现,GDC应该与其差别不大).

D主要有三种分配内存的途径.

  • 静态数据,分配在默认数据段上.
  • 堆栈数据,分配在线程堆栈上.
  • 垃圾收集数据,动态分配在GC Heap上.

前面的两种与C/C++没什么区别.我们主要讨论第三种.

先来看看C/C++的内存管理模式.

  1. 首先我们会为某个对象或类型分配内存.
  2. 初始化上一步所得的内存.
  3. 通过访问对象或类型成员来使用资源.
  4. 销毁资源状态,执行资源清理工作.
  5. 释放内存.

这种模式看起来相当的简单,但是却是导致很多问题的根源.想想有多少次我们忘记了释放无用的内存.想想有多少次调试的时候,内存问题是浪费我们时间的最大元凶.

GC正是为了解决该问题而产生的.它接管了上述步骤的第五步.对于一些本地资源(比如文件,数据库连接,套接字,同步对象,位图,图标等),通常在其对象的内存即将被回收时,必须执行一些资源清理工作.

下面来探讨一下D语言的GC内存分配和资源初始化.所有继承自Object的对象均在GC Heap上分配,此外还有动态数组.

D运行时内部有一个GC类.当应用程序的进程完成初始化后,GC也跟着初始化.GC内部会保留一块连续的地址空间,这段空间最初并不对应任何物理内存.该地址空间即为GC Heap.GC Heap上维护着一个指针,该指针表示下一个新建对象分配时在GC Heap中所处的位置.开始时,该指针被设置为保留地址空间的基地址.

当我们new一个对象时,D运行时会调用Object _d_newclass(ClassInfo ci)函数来创建一个新的对象.至于该函数实现细节,因为没有找到相关代码,所以无法具体描述.我们可以猜测其实现细节如下:

  1. 计算类型所需要的字节数.此外,应该还有相关的一些额外开销所需要的字节数.这些额外开销应该包含有一个类型对象指针和一个同步索引块(该同步索引块会在后面详细解说).
  2. D运行时检查保留地址空间是否满足分配新对象所需要的字节数.如果需要则commit物理内存.如果GC Heap中还有足够的剩余空间,那么对象将被分配在GC内部维护指针所指示的地方,并且所分配的地址空间中的字节被清零.接着,调用类型的实例构造器返回对象的内存地址(GC内部维护的指针会传递给this参数,而类型的静态构造函数会在main()函数前调用完毕).而在new返回对象的地址之前,GC内部维护的指针会越过对象所处的内存区域,并指示出下一个新建对象在GC Heap中的地址.

这与C运行时库中的堆分配内存有着本质的区别.这样的内存分配方式使得分配对象的速度相当快,几乎可以与在线程堆栈中分配对象一样快!

接下来我们来看一下D语言中GC是如何工作的.

每个应用程序都有一组root.当GC开始执行时,它假设GC Heap中所有的对象都是垃圾.这样GC会查找所有的root(比如静态字段,方法参数,局部变量,CPU寄存器).如果发现root引用了一个对象,那么就mark它.接着GC继续查找,如果GC试图将一个先前已经标记过的对象再次mark时,它会停止该对象标记的路径方向上的遍历活动.这种行为有两个目的.首先,可以避免GC多次遍历;其次,如果对象之间出现了循环引用,可以避免陷入无限循环.

GC一旦检查完所有的root,便会回收那些未被mark的对象内存.接下来就到了压缩阶段.GC可能会压缩内存,也可能不会.这取决于找到的内存块容量.当GC找到比较大的连续内存块时,会把内存中的一些非垃圾对象搬移到这些连续内存块以压缩GC Heap.

显然,搬移内存中的对象将使所有所有包含这些对象指针的变量和CPU寄存器变得无效.因此,GC需要重新访问所有的root,并修改他们以使其指向这些对象的新内存位置.另外,如果对象包含有指向另一移动过的对象的字段,那么GC也会负责矫正这些字段.在GC Heap中的内存被压缩之后,GC内部维护的指针将被设置为指向最后一个非垃圾对象之后.

垃圾收集会给应用程序带来相当的性能开销,这也是使用GC时主要的负面影响.所以,GC会有一些特殊的设计来大幅提高GC的性能.D语言的GC是个基于代的垃圾收集器.引入代的唯一目的就是为了提高GC的性能.

前面讲到在创建新对象时,会创建一个同步索引块.它的目的是为了线程同步.当我们使用synchronized来创建线程安全的程序时,D运行时会把通过该同步索引块关联到一个同步锁.Windows下会关联到一个CRITICAL_SECTION,Linux则关联到一个pthread_mutex_t.这个不能用于多进程同步.D语言的GC实现了多线程安全.

此外,D语言的析构函数不表示确定性析构,但是通过delete表达式却实现了确定性析构的效果.当一些昂贵的资源需要及时析构时,采取这种方式是有效的.大多数的资源交给GC来回收即可.

唯一担心的是这种方式的滥用,因为确定性析构很可能会造成应用程序性能的下降.这会加大GC的压力,使得GC频繁的进行垃圾回收.建议采用.NET的终结模式.

评论

论D语言数组

这几天花了大量的时间来测试D语言数组,真是无聊哪,哈哈。

在前面的帖子中,我已经说明了D语言与C#数组访问之间的性能比对。
经过这几天的测试来看,基本上还是那个结果,而且也证明了D确实是跟C兼容性很强。

D语言的数组分为静态和动态数组,C#则没有这个区分。
在D中,除了使用static修饰的静态数组和全局静态数组之外,局部静态数组均分配在栈上,而动态数组,不论怎样,均会分配到堆上。这主要是因为动态数组要在运行时才能清楚内存分配的大小。C#数组则除了显式声明在栈上分配之外,都在托管堆上进行分配。
此外,如果要在D中声明在栈上分配动态数组,需要借助指针和C函数alloca。

比如

module sample;int[10] arrayA;//在全局存储区分配

int main(char[][] argv)

{

    int[10] arrayB;//局部静态,在栈上分配

    int[] arrayC;

    arrayC.length=10;//在堆上分配 

    static int[10] arrayD;//在全局存储区分配

    static int[] arrayE;

    arrayE.length=10; //在堆上分配

}

D创建静态数组的时间要快于动态数组,且一维数组均是连续内存分配。访问一维数组,和使用指针访问速度差不多(实际上要略慢于指针)。创建数组的时间大约跟迭代访问一遍的时间相仿(数组越大越相近,但是实际情况中往往小数组的使用更加频繁)。

比如

module sample;

int main(char[][] argv)

{

    int[10] arrayA;//创建速度最快,如果是静态数组推荐这种声明方式   

    int[10] arrayB = [0,1,2,3,4,5,6,7,8,9]; //很快,因为增加了赋值操作,赋值消耗小   

    int[10] arrayC = new int[10]; // 这种情况下,耗时相当多,数组越大越耗时   

    int[] arrayD;   arrayD.length=10; //很快,与静态数组初始化声明相当

}

值得一提的是关于new操作符,它并不能指明内存分配方式,比如上面的arrayC,仍然分配在栈上。但是却无端使得创建时间增长。此外,各数组的访问速度几乎是一样的。用指针访问速度稍快,尤其在大数组的情况下,但是同理,小数组使用更加频繁。C#的一维数组创建与访问速度与D相比稍慢,但相差不大。特别指出的是C#采用指针访问速度相当快,猜测是JIT做了优化。毕竟Native编译与JIT编译还是有差距,这是没办法回避的。

上面所说的针对一维数组,下面讨论多维数组。

D多维数组跟C完全一样。静态,动态分配符合上面说的情况。我主要来讨论一下多维动态数组。

D的多维数组实际上和C#中的交错数组一样,而不是C#中的多维数组。从内存分配上来看,D多维数组分配在堆上,且不连续。它很像数组和链表的结合,即各列的地址不连续,而各行的地址连续。C#中的交错数组也如此。而C#中的多维数组,内存连续,最大的好处和坏处是可以使用指针访问。这样,前面帖子中的比较结果就出来了。D多维数组的访问速度确实没有C#采用的指针访问快(这里还有JIT的功劳),但是却比C#常规模式快的多,大致是C#交错数组的2倍,多维数组的3倍。

Update : 2008-01-04

在最新的DMD 2.009版本中,D的多维数组已经跟C#的多维数组一样了,都是连续的内存分配。

综合来看,C#虽然可以采用指针的方法来优化访问数组速度,但是D使用指针的地方远比C#中多且好用。即使不用指针,D也要比C#快很多。

评论

D语言的陷阱

关注D语言已一月有余。最近又在翻看D语言规范,写些心得,以资纪念(本文代码采用C#命名规范)。

诚如D所介绍的那样,它是一门通用的系统和应用编程语言。俺最欣赏D能以原生语言的身份引入垃圾回收机制。不依赖于特定虚拟机的实现着实让俺兴奋了一阵。 垃圾回收是个古老话题,它的好处自不待言,N多语言都提供这种机制,但在原生语言中引入仍是凤毛麟角。听说C++0x标准正在准备引入垃圾回收机制,无疑D已经在这方面先行一步。

D借鉴了很多语言的长处,但在很大程度上保留了C/C++的观感。为了与C二进制兼容,采用了C99的数据类型;为了支持多种编程范式,沿袭了C++的模型。其中值得一提的是它的虚方法调用机制师从于Java。俺所说的是D在OOP上的理解。

现代编程语言基本都提供了OOP的编程机制,即封装,继承和多态。先声明一下,在这里我们讨论的主要是语言层面的OOP。设计模式提及的OOP是在编程语言提供的OO机制上的升华,是代码如何有效组织,与语言上的OO机制有很大不同。D语言采用单根+接口的继承机制。在多态上主要使用虚方法表和多接口来实现,而数据封装则主要通过它的attributes。

OK,下面我们先来看下D语言attributes语法层面上的小陷阱。

Attributes的定义如下:Attributes are a way to modify one or more declarations(D语言的attributes是用来修饰一个或多个声明的方式).

它通常形式如下:

attribute declaration;	/* affects the declaration */
attribute:		/* affects all declarations until the next } */
  declaration;
  declaration;
  ...
attribute		/* affects all declarations in the block */
{
  declaration;
  declaration;
  ...
}

你可能会说,这不是已经解释的很清楚了吗?当然,对于1和3的声明方式,我们都很容易理解。但是第2种声明方式,我就犯迷糊了。我们不论在phobos还是tango库都可以找到大量的类似声明。

比如 fenv.d(为了方便观看,去除了注释):

/* 示例1 */
module std.c.fenv;
extern(C):
struct fenv_t
{
  version(Windows)
  {
    ushort status;
    ushort control;
    ...
  }
  ...
}
...
enum
{
  FE_INVALID = 0x01;
  ...
}
void feraiseexcept(int except);
...

再比如array.d。

/* 示例2 */
 module std.array;
 private import std.c.stdio;
 class ArrayBoundsError: Error
 {
   private:
     uint linum;
     char[] filename;
   public:
     this(...)
     {
       ...
     }
 }
 ...

查阅源代码,这些都很容易理解。但是,这跟文档明显有出入。如果这不是语法陷阱,那么就是写文档的笔误了。

上面的是开胃小菜,真正的大餐来了,呵呵。

看一下下面这个示例。

/* 示例3 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestClassA();
  return 0;
}
void TestClassA()
{
  A a = new A();
  printf("%*s",a.Method());/* 这里可以看出C和D处理字符串的区别 */
}
class A
{
  char[] Method(){return "Call member function Method() of class A.";}
}

函数TestClassA()会执行成功吗?答案是肯定的。因为在不带修饰符的情况下,D语言默认是public级别,不论对象是全局函数,结构还是类,成员函数。前面都好理解,但是连成员函数都默认是public,这就奇怪了。从OOP的角度来说,默认应该是保护级别的最大级别,尤其是在类中。在C++中,成员函数默认是private,这跟数据封装有关系。因为当程序员忘记修饰时,编译器会帮忙以免数据可以随意访问。当以后需求有变化时,再把它修正为public,这样对现存的客户程序都不会有兼容的问题。但是如果一旦把public修正为private时,麻烦就来了。继承的子类,客户程序等等都要在考虑之列。至于D为什么要把成员函数默认为public,俺不理解。另外俺认为良好的编程风格应该可以清晰表达代码的意图。D为了保持C/C++的观感,采取了上面的风格。俺不推荐。俺认为风格应该如下(以下所有的代码示例都会采用如下风格,并且除非采用C面向过程的结构化编程,不会再用到类似TestClassA()这种全局函数):

public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}

下面再看一下这段代码示例。

/* 示例4 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.TestClassA();
  return 0;
}
public class TestCase
{
  public void TestClassA()
  {
    A a = new A();
    printf("%*s",a.Method());/* 这里可以看出C和D处理字符串的区别 */
  }
}
public class A
{
  private char[] Method(){return "Call member function Method() of class A.";}
}

有过C++经验的程序员看到上面这段代码,会不会认为这是段错误代码,能通过编译吗?答案是上面这段代码不但能通过编译,而且运行良好。为什么会这样?D里面的private和C++/C#等语言private的语义稍有不同。在D中,private修饰的函数不仅可以被所在类的内部成员访问,甚至可以被同一模块内的其他成员访问。在同一模块内,它相当于C语言中被static修饰的函数,表达的是friend的语义。这一点跟Delphi很相似,只不过在Delphi中称其为单元(unit)。俺认为,D语言提供这个特性虽然方便了程序员编码,但也可能造成槽糕的代码组织和编程习惯。因为它破坏了OOP的封装性。所以,Delphi在其2005新版中增加了strict private来确保封装的严密。但在D中,目前还没有提供相似的功能。或许是D有意为之?俺建议,如果采用OOP,在模块内应人为限制private的语义(类C编程除外)。这是个无奈之举,最稳妥的办法是在语言机制上做出修改。

同理,protected也存在同样的问题。

到了这里,你可能会质疑示例3。D语言默认成员函数的访问级别应该是private才对啊,因为同一模块内,它可以随意访问。那么我们再修改一下示例3代码。

 

编译运行示例5,我们发现依然能运行成功。如果修改Method()为private级别,则不会编译成功。这就说明前面的分析正确。

下面,我们来讨论一下D的继承机制。

/* 示例6 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    TestClassA();
    TestClassB();
  }
  private void TestClassA()
  {
    printf("Call function TestClassA()...\n");
    A a = new A();
    printf("%*s",a.Method());
    printf("\n\n");
  }
  private void TestClassB()
  {
    printf("Call function TestClassB()...\n");
    B b = new B();
    printf("%*s",b.Method());
    printf("\n");
    printf("%*s",b.Method(1));
    printf("\n\n");
  }
}
public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  public char[] Method(int i){return "Call member function Method(int) of Class B.";}
}

从C++的角度来看,上述代码并没有任何错误。但是在D中却不能编译通过。原因是B中并不存在有函数匹配Method()原型,所以b.Method()会调用不成功。奇怪,B明明继承父类A的Method()了啊。怎么会不能编译?

下面让我们修改一下示例6的代码。

/* 示例7 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    TestClassA();
    TestClassB();
  }
  private void TestClassA()
  {
    printf("Call function TestClassA()...\n");
    A a = new A();
    printf("%*s",a.Method());
    printf("\n\n");
  }
  private void TestClassB()
  {
    printf("Call function TestClassB()...\n");
    B b = new B();
    printf("%*s",b.Method());
    printf("\n");
    printf("%*s",b.AnotherMethod());
    printf("\n\n");
  }
}
public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  public char[] AnotherMethod(){return "Call member function AnotherMethod() of Class B.";}
}

这下总算可以编译运行了。郁闷了吧,哈哈。为什么示例6不能编译,而示例7可以?我们注意到两个示例有点小小的不同,就是示例6有重载方法,而示例7则没有。Bingo!原因就在于此。D认为如果你要重载父类的方法,就必须显式的声明它。这是个良好的习惯,但许多程序员一开始都很不适应(Delphi和VB程序员似乎不会有这个问题,因为它们重载要显式声明),呵呵。我们再次修改示例6的代码,以便让其重载方法可以运行。

/* 示例8 */
module sample;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    TestClassA();
    TestClassB();
  }
  private void TestClassA()
  {
    printf("Call function TestClassA()...\n");
    A a = new A();
    printf("%*s",a.Method());
    printf("\n\n");
  }
  private void TestClassB()
  {
    printf("Call function TestClassB()...\n");
    B b = new B();
    printf("%*s",b.Method());
    printf("\n");
    printf("%*s",b.Method(1));
  }
}
public class A
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  alias A.Method Method;
  public char[] Method(int i){return "Call member function Method(int) of Class B.";}
}

最后,我们来看下D语言的多态。D语言实现多态主要是通过虚方法调用和多接口继承。此外,抽象类的使用也是实现多态的重要途径之一。多态问题非常复杂,很难一下说清楚。因此,我们重点考察D的虚方法调用和多接口继承(应用设计模式,抽象类也能发挥很大作用,但不在我们讨论范围之内)。

D语言的虚方法调用机制跟Java很相似,却与C++/C#背道而驰(这两种设计哲学孰优孰劣不予讨论)。D认为,所有非静态,非私有方法默认都是虚方法。需要说明的是,虚方法调用的开销要比非虚方法调用大的多。因此,D编译器在编译代码之前,会分析子类是否overridden父类的虚方法。如果没有,则编译成非虚方法。这样做的好处是不用再考虑应该把哪个方法设置为虚方法了,坏处是可能造成设计的不清晰和滥用。

接口既是表达多态的手段,也是实现契约编程的手段。接口实际上只是为一组方法签名指定一个名称的方式。这些方法根本不带任何实现。但是继承接口与继承父类截然不同。继承接口必须显式实现接口方法,而继承父类则不必显式实现。不管一个接口的契约说明有多么好,都无法保证任何人能100%正确实现它。COM就颇受这个问题之累,导致有的COM对象只能正确用于Microsoft Office Word或Microsoft Internet Explorer。此外,如果多个接口的方法签名相同,如何正确实现它也是个问题。值得注意的是,接口方法是虚方法。

下面的示例很好的说明了上述问题。

/* 示例9 */
module sampleford;
import std.c.stdio;
int main(char[][] argv)
{
  TestCase test = new TestCase();
  test.Test();
  return 0;
}
public class TestCase
{
  public void Test()
  {
    A a = new A();
    printf("%*s", a.Method());
    printf("\n");
    B b = new B();
    printf("%*s", b.Method());
    printf("\n");
    C c = new C();
    printf("%*s", c.Method());
    printf("\n\n"); 

    printf("---------Program executes succeeded.--------");
  }
} 

public interface IA
{
  char[] Method();
}
public interface IB
{
  char[] Method();
} 

public class A : IA
{
  public char[] Method(){return "Call member function Method() of class A.";}
}
public class B : A
{
  public override char[] Method(){return "Call member function Method() of class B.";}
}
/* C应该怎么实现 */
public class C : A, IA, IB
{
  /*
   * 奇怪的是竟然可以编译成功,不知道算不算是个Bug.
   * 但是调用不到这个方法.
   */
  alias A.Method Method;
  /*
   * 这个方法到底是谁的实现
   * 遗憾的是D还没有提供显式接口实现的特性
   * 所以目前不能区分到底实现的哪个接口方法
   */
  public override char[] Method(){return "Call member function Method() of class C.";}
}

D语言存在的陷阱不在少数。比如指针的陷阱,虽然比C++中减少了很多,但是只要是指针,就不可避免的存在问题,甚至新增了一个指向垃圾收集堆的新问题,幸运的是我们大部分情况下不需要动用指针这个超级武器。比如泛型编程,泛型已经逐渐成为编程主流,但是D当中的模板依然存在一定问题(这些问题有时间再撰文讨论)。俺只是讨论了D在OOP当中应该注意的问题,这些问题在其他编程语言中也或多或少的存在。

评论