Introduction
A very common paradigm when constructing C++ classes is that
of using set/get member functions to access private member variables. This paradigm allows for expressing
const-correctness, input validation, and controlling read/write synchronization
among other things. That said, what is
the most efficient way of creating a setter?<o:p>
A setter is, by convention, a function that
usually takes a single parameter, optionally performs some sort of input
validation, and assigns the parameter’s value into the private storage for that
value. Simple,
right?
Measuring Performance Given the number of setters any given application may have,
it is important to have some general uniform guidance on the best way to
implement and use them. How many
variations of a “setter” signature could there be? For this common form of the setter, I have
laid out twelve variations and use cases.
Each one was baselined against a simple assignment as if the member
variable were public. The experiments
were created and executed using a C++ benchmarking library called Celero and have been included in Celero’s baseline on GitHub as a
demonstration project.<o:p>
The table below ranks the relative performance of each
call->signature->implementation configuration. Note that these experiments utilized a
non-POD type (std::string, in this case).
Call<o:p>
| Signature<o:p>
| Implementation<o:p>
| Multiple of Baseline
(Lower is Better)<o:p>
|
foo->var = x;<o:p>
| (N/A)<o:p>
| (N/A)<o:p>
| 1.00<o:p>
|
foo->var = std::move(x);<o:p>
| (N/A)<o:p>
| (N/A)<o:p>
| 0.89<o:p>
|
foo->set(x);<o:p>
| set(std::string x)<o:p>
| this->var = x;<o:p>
| 1.39<o:p>
|
foo->set(std::move(x));<o:p>
| set(std::string x)<o:p>
| this->var = x;<o:p>
| 1.27<o:p>
|
foo->set(x);<o:p>
| set(std::string x)<o:p>
| this->var = std::move(x);<o:p>
| 1.34<o:p>
|
foo->set(std::move(x));<o:p>
| set(std::string x)<o:p>
| this->var = std::move(x);<o:p>
| 1.13<o:p>
|
foo->set(x);<o:p>
| set(const std::string& x)<o:p>
| this->var = x;<o:p>
| 1.00<o:p>
|
foo->set(x);<o:p>
| set(const std::string&& x)<o:p>
| this->var = x;<o:p>
| 0.97<o:p>
|
foo->set(x);<o:p>
| set(const std::string&& x)<o:p>
| this->var = std::move(x);<o:p>
| 0.97<o:p>
|
foo->set(x);<o:p>
| set(std::string&& x)<o:p>
| this->var = x;<o:p>
| 0.97<o:p>
|
foo->set(x);<o:p>
| set(std::string&& x)<o:p>
| this->var = std::move(x);<o:p>
| 0.89<o:p>
|
foo->set(std::move(x));<o:p>
| set(std::string&& x)<o:p>
| this->var = std::move(x);<o:p>
| 0.89<o:p>
|
foo->set(x);<o:p>
| set(std::string&& x)<o:p>
| std::swap(this->var, x);<o:p>
| 1.07<o:p>
|
Recommendation
The penalties for getting a setter wrong can be high. Worst case, you could be paying up to 40%
more for a call than necessary. And,
while setters are likely not bubbling to the top of your hot-spot list, having
many of these throughout the application can add up to a very unnecessary
performance hit.
<o:p>
My recommendation, in general, would be to use the const
reference L-Value form of the setter.
However, if performance is absolutely necessary, you can also provide a
pass-by R-Value which, internally, uses std::move. (There are types, however, that are not
inexpensive to move. Be careful.) <o:p>
Notes
- If a POD type such as int were used, a modern compiler (Visual
Studio 2012, in this case) did a good job of making all of them perform nearly
identically well.
- Celero Project on Git Hub (Please Star!)
History
March 2014 - First Published.