The Implementation of Value Types - Lawrence Crowl - CppCon 2014
The Implementation of Value Types - Lawrence Crowl - CppCon 2014
Value Types
Lawrence Crowl
CppCon 2014
Introduction
type(type&& a)
: p(a.p) {
a.p = std::nullptr;
}
Parameter Passing
Two Choices
Pass by value.
Pass by const reference.
Slicing
class B {
virtual bool d() { return false; } };
class D : B {
virtual bool d() { return true; } };
bool g( B a) { return a.d(); }
bool h(const B& a) { return a.d(); }
g(D()) == false && h(D()) == true
Pass by Value
extern type va1(type input);
extern type va2(type input);
void vf1(type& output, type input) {
output += va1(input);
output += va2(input);
}
Direct Pass by Value
Requires a trivially copyable class.
Copy argument to the stack (e.g. IA32).
Work is equivalent to a memcpy.
Copy small arguments to a register (e.g. AMD64).
May spill argument registers.
Do neither (e.g. SPARC32).
Instead use indirect pass by value.
Indirect Pass by Value
Works with non-trivally copyable types.
Allocate a temporary local variable of the type.
Copy the argument to the temporary.
Pass a pointer to the temporary into the function.
Access the content of the parameter indirectly.
Deallocate the temporary on return.
Pass by Const Ref
extern type ra1(const type& input);
extern type ra2(const type& input);
void rf1(type& output, type& input) {
output += ra1(input);
output += ra2(input);
}
Parameter Recommendations
Pass by value when the type is
small (<= 2 pointers) and
trivially copyable.
Pass by const ref when
the type is large and
alias detection is relatively cheap.
Otherwise do some experiments.
Result Passing
Return by Value!
Named Return Value Optimization
type function(....) {
type result;
....
if ( .... ) {
....
return result;
}
....
return result;
}
Aliasing
Approaches
Ignore the problem.
Document the problem.
List possible overwrites in the comments.
Use the restrict qualifier that C++ does not have.
Overcome the problem.
Copy a Potentially Aliasing Parameter
void rf3(type& output, const type& input) {
type temp = input;
output += rf1(temp);
output += rf2(temp);
}
Conditionally Copy
void rf3(type& output, const type& input) {
if ( &output == &input ) {
type temp = input;
output += rf1(temp);
output += rf2(temp);
} else {
....
}
}
Conditionally Do Not Copy
type& type::operator =(const type& a) {
if ( this != &a ) {
delete p;
p = new content(*a.p);
}
}
Order Reads Before Writes
void rf4(type& output, const type& input) {
type temp1 = ra1(input);
type temp2 = ra2(input);
output += temp1;
output += temp2;
}
Aliasing Fields
template <typename T>
T& complex<T>::operator *=(const T& a) {
real = real * a.real - imag * a.imag;
imag = real * a.imag + imag * a.real;
return *this;
}
Read Caching Fields
template <typename T>
T& complex<T>::operator *=(const T& a) {
T a_real = a.real, a_imag = a.imag;
T t_real = real, t_imag = imag;
real = t_real * a_real - t_imag * a_imag;
imag = t_real * a_imag + t_imag * a_real;
return *this;
}
Conflicts
Global State
Changes to global state must not affect logical operation
results.
Access to constant state is fine.
Memory allocation is fine.
Physically shared state must be protected against concurrent
access.
Operations must not affect global state.
Physically shared state must be protected against concurrent
access.
I/O only for debugging and performance analysis.
Static Initialization Order
constexpr type::type(int arg)
: field(arg) { }
type v(3);
Concurrency
Reduce aliasing otherwise.
Make const reference parameters concurrent-read safe.
The deep argument is only read in all operations, or
any access is protected with locks or atomics.
Exceptions
Where possible, make operations noexcept.
Otherwise, make operations exception safe.
An exception leaves the object in the state it had at the
beginning of the operation..
Allocate Before Changes
type& type::operator =(const type& a) {
if ( this != &a ) {
content *q = new content(*a.p);
delete p;
p = q;
}
}
Recover Resources
type& type::operator =(const type& a) {
if ( this != &a ) {
content *q = new content(*a.p);
try {
delete p;
} catch ( ... ) {
delete q;
rethrow;
}
p = q;
}
}
Optimization
Responsibilities
You
choose the representation;
implement the operations;
reduce aliasing; and
reduce memory accesses.
The compiler
does most everything else.
Avoid Redundant Memory Access
Loading a pointer twice is inefficient unless you are
waiting for someone else to change it.
The keyword this is an implicit pointer and inhibits
optimization.
Aggressively cache field reads.
Write back cached field.
Inlining
Inlining may be a long-term commitment.
Constexpr implies inlining.
Inlining increases code bloat and puts pressure on the
cache.
Inline when the body is no bigger than the call.
Otherwise, inline when you have evidence of
performance gains.
Portability
IBM System\360
Follow Along
Follow the standard.
Follow the compilers.
Follow the authors.
Follow the tools.
Choose Portable Types
int64_t num_humans;
for ( size_t i = 0; i < v.size(); ++i )
.... v[i] ....;
int c = getchar();
Summary
Invest