本文译自Dan Saks的专栏文章“More ways to map memory”
最优雅、最让人愉悦的表示方法不一定是最高效的。
这是关于访问映射到内存的设备寄存器系列文章的第三篇。在笔者的前一个专栏中,讨论了映射到内存的几种不同方法,这些方法可能导致编译出不同效率的目标代码。一些读者的反馈使得笔者对这些研究作了一些改进。这次,笔者将把这些反馈与大家共享,并展示一些其他的方法。
上期回顾
在标准C/C++中,通过deference(不好译啊……)一个存放寄存器地址的指针来访问映射到内存中的设备寄存器是具有代表性的一种方法。可以使用宏或者const对象来定义这个指针。
正如在前一个专栏中,笔者使用了一个跑在ARM Evaluator-7T单板机上的例子。该板子的文档使用特殊寄存器来表示寄存器,因此笔者也这样做。Evaluator-7T的地址是可以以字节为单位寻址的,但是每个特殊寄存器都是4个字节的。它们同时还是volatile的,因此可以如下定义:
typedef unsigned int volatile special_register;
Evaluator-7T使用了5个特殊就采取来控制两个集成的定时器,如下:
typedef struct dual_timers dual_timers;
struct dual_timers
{
special_register TMOD;
special_register TDATA0;
special_register TDATA1;
special_register TCNT0;
special_register TCNT1;
};
定时器的寄存器从Evaluator-7T板上地址0x03FF6000处开始。程序可以通过如下定义的指针来访问这些寄存器:
#define timers ((dual_timers *)0x03FF6000)
或者:
dual_timers *const timers = (dual_timers *)0x03FF6000;
清除或设置TMOD寄存器的某些位可以禁用/激活该定时器。因此可以用枚举为这些位定义一些掩码:
enum { TE0 = 0x01, TE1 = 0x08 };
同时禁用这两个定时器:
timers->TMOD &= ~(TE0 | TE1);
当定义成宏时,timers被展开成一个rvalue表达式。如果被定义成一个const指针,timers是lvalue表达式(译注:此处应注意T const *和T * const的区别). lvalue是指能够得到某块可修改的内存区的表达式,除lvalue外的表达式都是rvalue(译注:rvalue是指不可修改的某块内存区以及一些字面值,如const对象,数字1,等等)。因为rvalue不一定表示一个对象(一块内存),编译器可以避免为rvalue来分配存储空间。避免为lvalue分配空间很难,但是可能的。
在C中,在全局作用域上声明为const对象默认的linkage是外部的,就好像使用extern修饰了一样。这意味着对timers的引用可能会出现在其他的翻译单元(译注:此处可以理解为编译单元),因此C编译器必须为timers生成能够在外部访问的存储。
在C++中,在全局作用域上声明为const对象默认的linkage是内部的,就好像使用了static修饰一样。这意味着对timers的引用必须出现在定义它的翻译单元。在这种情况下,编译器可能会认为不需要为这个const指针分配存储。C编译器应当能够通过static关键字消除为const指针分配的存储。
笔者写了一些小程序来测试现实中编译器上面的描述。列表1的程序中把timers定义为一个宏。用了x86上4个不同的编译器来编译该程序。在所有的情况下,编译器为指针值使用了立即操作数,而且没有在数据段保留该const指针的拷贝。
列表1:一个小测试,用来观察编译器如何生成访问映射到内存的设备寄存器的代码:
typedef unsigned int volatile special_register;
typedef struct dual_timers dual_timers;
struct dual_timers
{
special_register TMOD;
special_register TDATA0;
special_register TDATA1;
special_register TCNT0;
special_register TCNT1;
};
enum { TE0 = 0x01, TE1 = 0x08 };
#define timers ((dual_timers *)0x03FF6000)
int main()
{
timers->TMOD &= ~(TE0 | TE1);
timers->TDATA0 = 50000;
return 0;
}
在第二个测试程序中,用const指针替代了宏:
dual_timers *const timers = (dual_timers *)0x03FF6000;
C编译器总是在数据段为该指针保留了一分拷贝,但是C++编译器不会。
在第三个测试中,用static关键字修饰上面的const指针定义:
static dual_timers *const timers = (dual_timers *)0x03FF6000;
对于C++编译器产生的代码没有影响。C编译器本应当能够使用这个来产生更好的代码,但是只有一个编译器这么做了。
一个被遗忘的观察
上次笔者写道:
“在C中,const对象……在全局作用域声明时默认的linkage是外部的。也就是说,它们的行为表现得像用extern声明的……这表示对timers的引用可能会出现在其它的翻译单元中,C编译器必须为timers分配存储,就像存在外部的引用。理论上,linker可能会判断没有外部引用而消除timers的存储,但是笔者不知道有哪个linker会这样做。”
Dave Baker提到C编译器会在link时丢弃没有用到的对象。笔者意识到,上次分析时,笔者仅仅观察了生成的汇编代码,而不是连接后的可执行程序。当笔者检查每个测试程序的连接映射时,发现所使用的C编译器中,有一个linker丢弃了没有使用的指针。
使用局部指针
迄今为止,所有测试过的程序把timers声明为一个非局部的名字。然而,作为一个通用规则,应该将名字声明在一个尽可能小的作用于。因此笔者将timers定义为一个main函数的局部const指针,如下:
int main()
{
dual_timers *const timers
= (dual_timers *)0x03FF6000;
...
}
通过这种方法,C和C++编译器生成的代码与把timers定义成宏的代码相同。也就是说,编译后的代码没有为const指针分配任何空间,用的是立即操作数来作为指针的值。第三个编译器也一样,但是实际生成的代码与把timers定义成宏的代码有少许不同。
令人惊讶的是,第四个编译器对使用局部指针的代码所生成的代码明显比定义成宏的方式要差。编译后的C程序在栈上为该指针分配内存,然后在运行时初始化它。生成的代码也比用宏的方式使用了更多的指令。
当笔者将timers定义成main局部的静态const指针:
int main()
{
static dual_timers *const timers
= (dual_timers *)0x03FF6000;
...
}
笔者得到的代码与把该指针定义成全局指针相同。这对于笔者所测试过的编译器都是一样的。
使用引用而不是指针
C++中的引用提供了与指针相同的能力。与指针相似,引用也可以用来间接引用一个对象。两者的区别在于指针需要用“*”来deference,而引用会自动deference对象。
引用本质上是const指针(而不是指向const的指针),每次使用时都会自动deference。永远都可以用引用来改写使用const指针对代码。如:
int &ri = i;
与下面的定义等价:
int *const pi = &i;
对引用的赋值:
ri = 4;
与用指针进行显示deference的赋值相同:
*pi = 4;
在C++中,可以使用引用来引用映射到内存的设备寄存器,如:
dual_timers &timers = *(dual_timers *)(0x03FF6000);
把timers声明为指向位于地址0x03FF6000上的dual_timers对象。因为引用会被自动的deference,不需要使用“->”操作符,如:
timers.TMOD &= ~(TE0 | TE1);
笔者更喜欢这些映射到内存的寄存器看起来像对象而不是指针。用全局引用来定义timers并用C++编译器来编译,生成的代码与timers被声明为const指针时相同,然而不全是这样。
当timers如下定义:
dual_timers &timers = *(dual_timers *)(0x03FF6000);
所有的编译器都为timers分配了内存,就好像如下定义:
dual_timers *const timers = (dual_timers *)0x03FF6000;
注意这只能在C++中使用,因为C不支持引用。在C++中全局的const对象的linkage是内部的,然而,引用没有定义为const,因此有外部的linkage,就像C中的全局对象。将timers定义为一个引用的C++代码与将其定义为const指针并用C编译器编译更相近。
使用其中一个编译器,使用全局引用的C++代码所生成的代码与用全局const指针的C代码是完全一样的。另一个编译器用不同的方式使用CPU寄存器,比用C中const指针产生了更多的指令。剩下两个编译器对于引用的处理实在令人惊讶:产生的代码在运行时初始化引用而不是编译室,导致产生了更大而且更慢的代码。
如下所示,使用static来修饰引用定义:
static dual_timers &timers = *(dual_timers *)0x03FF6000;
该段代码赋予timers内部linkage但对生成的代码没有别的影响。
最终,笔者将该引用定义为main的局部变量:
int main()
{
dual_timers &timers
= *(dual_timers *)0x03FF6000;
...
}
使用这种方法,所有的C++编译器产生的代码与将timers定义成一个局部const指针相同。
好了,那什么是新的底线?从笔者有限的示例中看出,好像将const指针定义为一个宏更能保证编译器为访问映射到内存的寄存器产生最紧凑的代码。然而,将指针定义成局部的const可能是比用宏更好的风格,而且对于大多数编译器来说,产生的代码与用宏一样好。在C++中,使用引用为内存映射提供了更好的记号,但对于许多编译器,这可能会导致性能上的小惩罚。
对于那些想检视一下自己编译器的程序员,列表2显示了笔者所有的测试程序。可以使用宏VER来选择其中的一个。
列表2:
typedef unsigned int volatile special_register;
typedef struct dual_timers dual_timers;
struct dual_timers
{
special_register TMOD;
special_register TDATA0;
special_register TDATA1;
special_register TCNT0;
special_register TCNT1;
};
enum { TE0 = 0x01, TE1 = 0x08 };
#if VER == 1 // a macro
#define timers ((dual_timers *)0x03FF6000)
#elif VER == 2 // a global const pointer
dual_timers *const timers = (dual_timers *)0x03FF6000;
#elif VER == 3 // a global const pointer declared static
static dual_timers *const timers = (dual_timers *)0x03FF6000;
#elif VER == 6 // a global reference
dual_timers &timers = *(dual_timers *)0x03FF6000;
#elif VER == 7 // a global reference declared static
static dual_timers &timers = *(dual_timers *)0x03FF6000;
#endif
int main()
{
#if VER == 4 // a local const pointer
dual_timers *const timers = (dual_timers *)0x03FF6000;
#elif VER == 5 // a local const pointer declared static
static dual_timers *const timers = (dual_timers *)0x03FF6000;
#elif VER == 8 // a local reference
dual_timers &timers = *(dual_timers *)0x03FF6000;
#endif
timers->TMOD &= ~(TE0 | TE1);
timers->TDATA0 = 50000;
return 0;
}
Dan Saks is president of Saks & Associates, a C/C++ training and consulting company. Dan is co-author of C++ Programming Guidelines and co-developer of Suite++: The Plum Hall Validation Suite for C++. You can write to him at and find him on the web at .