Eliminating indirection in C
Recall the pointer-based C implementation of the timer set function:
void timer_registers_set
(timer_registers *this, count_type c)
{
this->TDATA = c;
this->TCNT = 0;
}
If you assume there's only one timer, you can remove the first parameter, this, from the function's parameter list and replace every reference to this-> with a reference to that one timer, as in:
extern timer_registers the_timer;
void timer_registers_set(count_type c)
{
the_timer.TDATA = c;
the_timer.TCNT = 0;
}
Because the_timer has a fixed address and structure member TDATA has a fixed offset, the compiler and linker can resolve the_timer.TDATA into an address known at build time. In contrast, the value of parameter this could be unknown at build time, so accessing this->TDATA requires a pointer-plus-offset computation at run time. This reasoning applies to accessing member TCNT as well.
Eliminating indirection in C++
The design I just described works in C++ as well as in C. However, you can implement it more elegantly in C++ by using a class with static members. Specifically, you can rewrite the timer_registers class so that every data member and every member function is declared static, as shown in Listing 2.
The static data members in a class exist independently of any objects of that class. As with other statically allocated objects, static data members have fixed addresses rather than offsets from the start of their class. Thus, static data members contribute nothing to the size of their class. All the data members in Listing 2 are static, so timer_registers objects will be empty. Interestingly, the C++ standard requires that the sizeof operator return something greater than zero, so sizeof(timer_registers) is at least one.
The declaration of a non-const static data member appearing within a class definition is not a definition—it doesn't allocate storage. In the case of the timer_registers class, this is good because we don't want the compiler deciding where to place static members TMOD, TDATA, and TCNT. These are memory-mapped registers that must be associated with specific memory addresses.
Static data members in C++ have external linkage. The linker sees their names. You should be able to use linker command options or scripting to bind static data members to specific memory addresses, much as you can with other objects declared extern. Alternatively, if your C++ compiler supports an extension that lets you place objects at a specific address, you should be able to apply it to static data members, as in:
class timer_registers
{
~~~
static device_register TMOD @ 0xFFFF6000;
static device_register TDATA @ 0xFFFF6004;
static device_register TCNT @ 0xFFFF6008;
~~~
};
Specifying an address for each device register, whether by a language extension or by the linker, is tedious and error-prone. Using an intermediate structure, as in:
class timer_registers
{
~~~
private:
struct T
{
device_register MOD;
device_register DATA;
device_register CNT;
};
static T @ 0xFFFF6000;
~~~
};
lets you specify just one address for the entire collection of registers. With some compilers, it may also facilitate further optimization, which I'll explain in an upcoming column.
Recall the implicitly-declared this parameter in a nonstatic class member function points to an object of the class type. However, the static data members for a class aren't in any one object—they're in their own statically-allocated storage. A member function that accesses nothing but static data members doesn't need a this parameter, and passing a pointer as the value for this is wasted effort. Static member functions eliminate that waste. A static member function is simply a class member function that doesn't have an implicitly-declared this parameter.
The definition for a static member function looks just like the definition for a nonstatic member function. For example, when the timer_registers set function is a static member, the definition looks like:
void timer_registers::set(count_type c)
{
TDATA = c;
TCNT = 0;
}
which is exactly the same as when set is a nonstatic member. You don't supply the keyword static in the definition because the language doesn't allow it there. However, the class declares set as static, so the function really has only one parameter, c. No this.
Since a static member function doesn't have a this parameter, a call to that function need not use the . (dot) or -> (arrow) operators to provide a value for this. However, if you just use the member name as the function name, as in:
set(timer_registers::TICKS_PER_SEC);
the compiler won't know that set is from the timer_registers class, unless the call takes place within the scope of the timer_registers class. Rather, you use the :: operator to provide the full name of the static member function, as in:
timer_registers::set
(timer_registers::TICKS_PER_SEC);
On the other hand, if you want to declare a timer_registers object and use it as the object in a static member function call, you can. For example, when set is a static member, you can still write calls such as:
the_timer.set(timer_registers::TICKS_PER_SEC);
In this case, the compiler looks at the expression to the left of the dot and finds the_timer, sees that the_timer is a timer_registers object, looks in the timers_registers class to find set, sees that set is a static member function, and then generates a call to set that ignores the_timer. The call generates the same code as when the function name appears as timer_registers::set. You can also call a static member function using a pointer and an ->, and the compiler will examine and ultimately discard the pointer in much the same way.
The ability to call a static member function using a dot or arrow as if it were a nonstatic member function is actually a nice feature: it lets you change the member functions from static to nonstatic and back without rewriting the calls to the member functions. This offers greater design flexibility down the road. I don't know any way to achieve the same flexibility in C.
More to come
The question still remains: does eliminating the pointers from the function calls and function bodies actually improve the run-time performance of code that manipulates memory-mapped devices? To answer this, I ran some timing tests using different C and C++ tool chains and made measurements, which I'll present next time. You may be surprised by some of the results.
Endnotes:
1. Saks, Dan. "Alternative models for memory-mapped devices," Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_6978.HTM.
2. Saks, Dan. "Memory-mapped devices as C++ classes", Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_7018.HTM.
3. Saks, Dan. "Compared to what?" Eetasia.com, March 2011. http://forum.eetasia.com/BLOG_ARTICLE_7028.HTM.
文章评论(0条评论)
登录后参与讨论