Examples from last time
datarep3/functionlocals.cc
: Infinite recursion and stack overflow
Understanding stringify
: call-by-value and copies
-
C++ is a call-by-value language
-
Function arguments and return values are copied as values
#include <cassert>
int f(int a) {
a = 2; // changes local variable, not caller’s version
}
int main() {
int a = 3;
f(a);
assert(a == 3);
}
Call-by-value and performance
- Call-by-value applies even to massive objects
#include <vector>
#include <cstdio>
unsigned long vector_sum(std::vector<unsigned long> vector) {
unsigned long sum = 0;
for (auto value : vector) {
sum += value;
}
return sum;
}
int main() {
std::vector<unsigned long> v;
for (unsigned long i = 0; i != 10'000'000; ++i) {
v.push_back(i);
}
printf("%zu\n", vector_sum(v));
}
- Copying can be a huge overhead!
- C++ compilers can avoid it sometimes using code analysis
Explicit references
- Programmers can also avoid copying overhead by passing pointers or references as arguments
- The callee function then shares the object in the caller
- Useful, but can be a bit risky!
#include <vector>
#include <cstdio>
unsigned long vector_sum(std::vector<unsigned long>& vector) {
unsigned long sum = 0;
for (auto value : vector) {
sum += value;
}
return sum;
}
int main() {
std::vector<unsigned long> v;
for (unsigned long i = 0; i != 10'000'000; ++i) {
v.push_back(i);
}
printf("%zu\n", vector_sum(v));
}
Other programming languages
- C++ isn’t the only language with issues around copying
- Java, Python are call-by-object languages: primitives are copied, “bigger” objects are passed by reference
- This can cause surprises too
Sorting integers: an example of the value of “working memory”
$ cd cs61-lectures/datarep3
$ make
$ ./intgen | python3 intsort.py
0
1
2
3
4
5
6
7
8
9
How call-by-value affects stringify
and returning local variables
- Our problematic version of
stringify
attempted to return a pointer to a local variable - The pointer was copied, but the pointed-to memory was not
- After the function exited, the pointed-to memory was reclaimed, and the pointer became poison: undefined behavior to touch
- Sanitizers often catch this error, not always
Fixing stringify
struct-stringify
std-stringify
intsort.py
import sys
ls = []
for line in sys.stdin:
val = int(line)
i = 0
while i < len(ls) and ls[i] < val:
i += 1
ls.insert(i, val)
for v in ls:
sys.stdout.write("{}\n".format(v))
intsort.cc
#include <cstdio>
#include <cassert>
#include <list>
#include <vector>
#include "hexdump.hh"
int main() {
std::list<int> ls;
// read integers from stdin, storing them in sorted order
int val;
while (fscanf(stdin, "%d", &val) == 1) {
auto it = ls.begin();
while (it != ls.end() && *it < val) {
++it;
}
ls.insert(it, val);
}
// print integers in sorted order
for (auto v : ls) {
fprintf(stdout, "%d\n", v);
}
}
intsort.cc
mark 2
- std::list<int> ls;
+ std::vector<int> ls;
What happened????
Here are some results from several runs of ./intgen -n 100000 | time ./intsort > /dev/null
(on my desktop in Docker):
Data structure | Elapsed times | ||
---|---|---|---|
std::list |
27.48 sec | 27.45 sec | 27.42 sec |
std::vector |
0.73 sec | 0.73 sec | 0.75 sec |
An investigation
- Let’s print out the integers’ addresses as well!
for (auto& value : ints) { print_object(value); }
- Sample output for
std::list
and./intgen -n 5
:5560'1407'2f50 00 00 00 00 5560'1407'2ef0 01 00 00 00 5560'1407'2ed0 02 00 00 00 5560'1407'2f30 03 00 00 00 5560'1407'2f10 04 00 00 00
- Sample start of output for
std::list
and./intgen -n 50000
:5626'6ed5'09d0 00 00 00 00 5626'6eda'8d50 01 00 00 00 5626'6ed5'6b90 02 00 00 00 5626'6edd'18f0 03 00 00 00 5626'6ec6'f090 04 00 00 00
- Sample output for
std::vector
and./intgen -n 5
:5621'd757'2f00 00 00 00 00 5621'd757'2f04 01 00 00 00 5621'd757'2f08 02 00 00 00 5621'd757'2f0c 03 00 00 00 5621'd757'2f10 04 00 00 00
- Sample start of output for
std::vector
and./intgen -n 50000
:7fe0'eb6c'6010 00 00 00 00 7fe0'eb6c'6014 01 00 00 00 7fe0'eb6c'6018 02 00 00 00 7fe0'eb6c'601c 03 00 00 00 7fe0'eb6c'6020 04 00 00 00
Analysis
std::vector
elements’ memory addresses are contiguous: next to one another, no gapsstd::list
elements’ memory address are not contiguous: big jumps, bigger the more integers there are- Contiguous memory is faster to access!
- Cache memory (unit 4)
- But why do
std::list
addresses bounce around so much?- Discuss!
Insertion order
Data structure | Insertion order | Elapsed times | ||
---|---|---|---|---|
std::list |
-r (random) |
27.48 sec | 27.45 sec | 27.42 sec |
std::vector |
-r |
0.73 sec | 0.73 sec | 0.75 sec |
std::list |
-u (increasing) |
7.43 sec | 7.43 sec | 7.36 sec |
std::vector |
-u |
1.16 sec | 1.14 sec | 1.15 sec |
std::list |
-d (decreasing) |
0.01 sec | 0.01 sec | 0.01 sec |
std::vector |
-d |
0.35 sec | 0.33 sec | 0.34 sec |
- The elements of a
std::list
are laid out in memory in increasing order by insertion time!- This means that when the input integers arrive in random order, traversing the list to find an insertion position involves many jumps to increasingly far-away points in memory—very bad for performance.
- But when input integers arrive in sequential order, then adjacent integers have nearby addresses—good for performance!
- Best for performance, though, is when no traversal is necessary at all (
-d
).
- The elements of a
std::vector
are laid out in memory in increasing order by position.- This requires moving elements around when inserting new elements, but we make up for that with efficient memory traversal.
- Only when inserting in descending order does
std::list
win.
- Can we do better still?