• No results found

Performance Trade-Offs and Evaluation

5.2 Time-Based STM for C/C++ Environments

5.2.2 Performance Trade-Offs and Evaluation

In this section, I will evaluate the performance of LSA for C++11 (Algorithm 3). The focus will be on scalability (i. e., how much performance a TM can provide when an increasing number of threads execute potentially conflicting transac- tions) and runtime overheads (i. e., how much overhead the TM introduces for transactional code compared to a nontransactional execution of the same code). However, how the TM maps from memory locations to orecs (i. e., the hash func- tion used in Algorithm 3 as well as the size of the array of orecs) can have a very large influence on performance. Therefore, I will investigate the performance trade-offs for this mapping and suitable hash functions first before looking at scalability and runtime overheads.

The machine used for the performance measurements in this section is a two-socket x86-64 NUMA system with two two six-core CPUs15 with hyper-

threading enabled. Thus, it provides 24 logical CPUs in total to the operat- ing system. Each socket represents one NUMA node. Benchmark threads are pinned to logical CPUs, alternating between NUMA nodes and first filling up CPU cores before making use of hyper-threading.16 This scheme will lead to NUMA-related slowdowns to be visible as soon as benchmarks are executed with multiple threads, and will not lead to hyperthreading-induced slowdowns until higher thread counts, when the likely higher level of contention causes memory to become more of a bottleneck.

The benchmarks that I will consider are Genome, KMeans-Lo, KMeans- Hi, Vacation-Lo, Vacation-Hi, and SSCA2 (see Table 3.2) as well as all the IntegerSet microbenchmarks (see Table 3.1). Note that I will show execution times for STAMP (i. e., less is better) but transaction throughput for IntegerSet (i. e., more is better).

The STM implementations that I evaluate here are part of TinySTM++. See Section 3.4.2 for further details about the implementations.

Memory–To–Orec Mapping

With orec-based STMs such as LSA, the detection of conflicts between transac- tions happens on the orecs and not on the original memory location. Thus, the memory–to–orec mapping affects every transactional memory access and thus the performance of the whole TM.

When choosing a mapping, we face several trade-offs. To keep the runtime overheads of transactions as low as possible, a coarse-granular mapping would be best so that a transaction has to acquire or validate as few orecs as possi- ble. However, a fine-granular mapping can help to avoid false conflicts between transactions (i. e., conflicts that exist on the level of orecs but not when consid- ering the actual accesses to application data), so that the TM does not provide less scalability than available in the workload. But if the workload does in fact not scale, then it can again be better to use a coarse-granular mapping so that transactions pass the bottleneck as quickly as possible.

To establish a coarse-granular mapping, we can either (1) map several mem- ory locations to the same orec or (2) increase the minimum granularity of mem- ory accesses considered for conflict detection. Using the latter to some extent is probably always required. For example, if most memory accesses in a trans- action target pointers, then we certainly do not want to acquire or validate a separate orec for each byte of each pointer; instead, when using machine words as minimum granularity, we only have to deal with one orec per pointer and still will not increase false conflicts if pointers are always aligned to words. Using a smaller granularity than machine words could help in some applications, but is probably not useful in general because many concurrent data structures are based on pointers or machine-word-sized integers (e. g., types such as size t). Also note that the granularity affects the other components of the mapping too because it changes the input values for those, as we will see later.

Another trade-off exists regarding the number of orecs that the TM should use. Using a large number of orecs allows for a potentially more fine-grained 16For example, thread 1 is on the first core of the CPU that comprises the first NUMA

node, thread 2 is on the first core of second NUMA node CPU, thread 3 is on the second core of the first NUMA node CPU, etc.; thread 13 shares the same CPU core as thread 1, thread 14 the same as thread 2, and so on.

concurrency control because the TM needs to map fewer memory locations to the same orec. However, a large number will also increase the space overhead, and can—depending on the hash function—increase a transaction’s footprint in caches and other hardware resources such as TLBs; especially the latter can also affect the performance of nontransactional code. On the other hand, a small number of orecs will less likely cause these problems but instead forces the hash function to try harder to avoid false conflicts.

Finally, the computational overhead of the hash function itself is also an important factor because it is executed once for each transactional memory access.17

This list of trade-offs already shows that choosing—or finding—a good hash function is nontrivial. What makes this even more difficult is that the input values to the hash function (i. e., the addresses of memory objects of the ap- plication) are controlled by entities outside of the control of the TM runtime library: Compilers, linkers, memory allocators, and the memory use patterns of the application itself. When the TM runtime library has to perform a transac- tional memory access, it cannot associate this access with a high-level memory object anymore (see Figure 5.2; alternatives based on compile-time analysis are discussed in Chapter 6).

Furthermore, the layout of application data in the address space affects the performance of more than just transactional code. For example, the memory allocator in the GNU C library (glibc) lets each thread allocate memory in different regions of the address space, which reduces the synchronization-related runtime overhead of memory allocation and can also lead to lower NUMA- related memory access costs by allocating the memory pages for those regions on the same NUMA node as the region’s thread. While this scheme can be a challenge for TM hash functions as we will see later, it leads to faster execution of the whole program. Thus, even if the TM could influence the memory allocation scheme, it would have to trade off benefits for transactions against benefits for everything else in a program. Second, it is not clear whether a tight integration between the TM and other system libraries provides enough benefits to justify the additional complexity and engineering costs. Therefore, I will assume that TM runtime libraries for the usage scenarios that I focus on cannot rely on being able to influence an applications use of the address space, and will just have to handle the current memory allocation schemes.

Hash functions. Algorithm 4 shows the hash functions that I will consider: Simple, Mult, and Mult32. All three have two common parameters: Shift (line 2) and the number of orecs (lines 3–4). The number of orecs must be a power-of-two value, and all hash functions return an index into the array of orecs (i. e., they return values in the range [0,orecs)). The Shift parameter represents the minimum access granularity discussed before; Shift controls how many of the least-significant bits of an address are discarded before the address is actually hashed (e. g., see line 13).

The Simple hash function then just uses the least-significant bits (of the remaining bits after shifting) to index into the array of orecs (line 14). While this 17This is to some extent a result of how the TM runtime library ABI is designed: Each

load or store function receives the absolut address or the target memory location, and not a relative address, for example.

Algorithm 4 Hash functions for mapping memory addresses to orecs on a 64b system.

1 // Global constants

2 unsigned shift; // The number of least−significant bits to discard

3 unsigned orecs bits; // The number of bits required to index into all orecs

4 unsigned orecs = 1 << orecs bits; // The number of orecs must be a power of two

5

6 // Odd random numbers. The examples given are the numbers used in the evaluation.

7 uint64 t randomMult = (11400714818402800990ULL >> shift) | 1;

8 uint32 t randomMult32 = 81007;

9

10 // The simple, default mapping

11 unsigned hash Simple(void∗ addr)

12 {

13 uintptr t a = (uintptr t)addr >> shift;

14 return a & (orecs − 1);

15 } 16

17 // Multiplicative hashing

18 unsigned hash Mult(void∗ addr)

19 {

20 uintptr t a = (uintptr t)addr >> shift;

21 unsigned pointer bits = sizeof(uintptr t) ∗ 8;

22 return ((a ∗ randomMult) >> (pointer bits − orecs bits − shift)) & (orecs − 1));

23 }

24

25 // Multiplicative hashing, 32b variant

26 unsigned hash Mult32(void∗ addr)

27 {

28 uint32 t a = (uintptr t)addr >> shift;

29 return (a ∗ randomMult32) >> (32 − orec bits);

30 }

is fast to compute, it does not consider any higher-order bits at all, which will result in two addresses being mapped to the same orec whenever their distance is a multiple of 2orecs bits+shift. This seems to happen rather often in practice. For

example, the per-thread regions in glibc’s memory allocator discussed previously are aligned to power-of-two addresses. If threads then have similar allocations patterns for shared data (as with the IntegerSet benchmarks), the probability of false conflicts rises the lower the number of orecs or access granularity.

The Mult hash function aims at avoiding the collision issues of the Simple hash function by using multiplicative hashing [68]: Instead of using just the least-signficant bits, it multiplies the address with a random number and uses the most-significant bits of this product (line 22). Note that it still discards the least-significant bits first, which reduces the number of bits that it can take into account (i. e., the number of bits by which to shift the product to the right is reduced by the Shift number of bits). The random number must be odd and in the range of possible inputs (i. e., [0, 264−shift) in a 64b system); the number

used in my implementation is based on the (√5 − 1)/2 recommendation by Knuth [68] (see line 7). The class of multiplicative hash functions with such constraints is universal in the sense that the probability of a hash collision for two different input values is bounded by 2/2orecs bits[32]. In practice, this means

that the TM can just pick a different random number—possibly at runtime—if it suspects the current level of false conflicts to be too high, and that doing so will with high probability lead to a better mapping eventually (if the first random number was indeed an unfortunate choice). Thus, compared to the Simple hash, Mult can avoid the 2n-difference collisions, can steer away from other collisions

by using a different random number, and is still relatively efficient to compute with just one 64b multiplication and a few additional bit-wise operations. It is also faster than other universal classes of hash functions (e. g., computing the address modulo a prime number).

The Mult32 hash function is a 32b variant of Mult and uses only the lower 32 bits of the address after discarding the access-granularity bits (line 28). This allows it to use just a 32b multiplication, which might be more efficient than Mult’s 64b multiplication on some hardware. The choice of a small random number for Mult32 (line 8) is not completely arbitrary: it represents a contrast to the large factor used in Mult, yet the value is large enough so that low-order bits of addresses will still make a difference.18

Interestingly, all three hash function result in about the same single-thread runtime overheads for transactions. Running the benchmarks with a Shift value of 6 and just 8 orecs (i. e., the whole array of orecs fits into a single cache line) on the machine described previously shows that the performance difference for any pair of hash functions is not more than 5% and typically around 2%. There is no clear winner, but Mult32 often performs best and Mult worst. The same holds when increasing the number of orecs to 212. This might be different on

less powerful CPUs.

Other candidates for hash functions could be xor-based functions that incor- porate knowledge about common layout patterns in the address space to mix input bits with low collision probability (e. g., to avoid the issues of Simple). Open hashing (i. e., keeping a linked list of several orecs per index when false conflicts happen) probably has a too large runtime overhead because of the in- direction and thus additional cache misses and footprint. Closed hashing and linear probing might be worthwhile, but I have not investigated this further.

Hash functions parameters. When we keep the random numbers used in Mult and Mult32 constant, all three hash functions are parametrized by Shift and the number of orecs. If we do not want to have to rely on automatic tuning of these parameters at runtime (which might be difficult), then we need to select parameters a priori that will hopefully provide decent performance.

I have measured the performance of LSA with each the Simple, Mult, and Mult32 hash functions, Shift ranging from 3 to 10, and the number of orecs ranging from 212to 226 on the STAMP and IntegerSet benchmarks with 4, 12,

and 24 threads, respectively. The performance of HashTable and SSCA2 is not significantly affected by the choice of hash function or hash function parameters, so I will not consider them in detail.

To be able to select good parameters, we first need to prune parameters that are unlikely to provide good performance; this would also be helpful if we could rely on automatic tuning.

Let us start this pruning process by looking at large numbers of orecs first; specifically, whether 224 or 226 orecs provide any benefit over 222orecs or less.

18Assuming that the smallest value of orecs bits is 16, then the 16 low-order bits of the

multiplication product will be discarded. For the least-significant bit of the address (after discarded access-granularity bits) to have a significant impact, the minimum value of the random number needs to be larger than 216; the value on line 8 is the product of (5 − 1)/2)

and 216. Note that for the experiments with 12orecs bits, a few further least-significant bits

of the address thus have less influence; nonetheless, the performance of Mult32 is still good in such a setting.

3 4 5 6 8 10 220 222 224 226 7 8 9 10 11 12 Genome, 12 threads Execution time (s) Number of orecs Shift 3 4 5 6 8 10 220 222 224 226 6 7 8 9 10 11 12 13 RBTree-Small, 12 threads Throughput (tx/ µ s) Number of orecs Shift 3 4 5 6 8 10 220 222 224 226 4.5 5 5.5 6 6.5 7 7.5 SkipList-Small, 12 threads Throughput (tx/ µ s) Number of orecs Shift

LSA-Simple LSA-Mult LSA-Mult32

Figure 5.3: Performance of LSA with different hash functions and hash param- eters for large numbers of orecs.

Figure 5.3 shows three interesting cases. In Genome, Simple can benefit only slightly from more than 222orecs, and just if Shift is less than 6. With the same benchmark but 24 threads (not shown), it can benefit a bit more and also with larger values of Shift. However, in all other STAMP benchmarks Simple does not benefit from large numbers of orecs, or is even slowed down by them (e. g., in Vacation). Mult and Mult32 never benefit or are slowed down, especially with small values of Shift as shown for Genome. The reason for the latter is likely that multiplicative hashing returns values that are distributed more randomly across the whole array of orecs, in contrast to Simple which translates locality in accesses to locality in the array of orecs; The result of such behavior is that Mult and Mult32 access a wider range of memory, and then probably suffer from the higher TLB footprint, for example. A small access granularity (i. e., a small Shift value) increases this problem because transactions then need to touch more orecs. A similar effect is visible with the IntegerSet benchmarks, but less pronounced; overall, Mult and Mult32 already reach peak performance with 222 orecs or less.

In contrast, Simple can benefit from large numbers of orecs on the IntegerSet benchmarks, as shown by the RBTree-Small and SkipList-Small examples in Figure 5.3. RBTree-Small suffers especially with a small Shift, which indicates that this is caused by false conflicts for pairs of addresses that have a power- of-two distance; LinkedList-Small and LinkedList-Large show similar patterns. SkipList-Small does not show this problem but is slowed down if Shift has a value of around 5 (RBTree-Large and SkipList-Large are similar); however, with a different Shift value, 222orecs can also result in decent performance.

Overall, performance does not increase when using more than 222 orecs.

Second, the space overhead would just be too large to be practical, as 222orecs

already require 32MB of memory on a 64b system. Therefore, I will not consider more than 222orecs.

In contrast, 212 orecs result in a space overhead of just 32KB, which might

be sufficient even on systems with a small amount of memory. However, do they allow for decent performance and infrequent false conflicts? Figure 5.4 shows a few examples.

For STAMP, Mult32 provides good performance in comparison to the other hash functions: It is always better or as good as the others in Genome and Vacation, but struggles with large Shift values in KMeans (but not significantly except in KMeans-Hi with 12 or 24 threads, where Mult is better). With In- tegerSet, Mult32 is always better than Simple. Mult is somewhat better than Mult32 in LinkedList but there is no clear winner among the two (see Fig- ure 5.4): Whereas Mult32 is typically better when Shift is less than 5, Mult performs equal or better starting at Shift values larger than 5. Overall, for 212

orecs, Mult32 with Shift set to 4 provides good performance compared to Mult or Simple with any Shift setting. Mult with Shift set to 6 also performs well, but struggles in Vacation. Simple with Shift set to 6 works well in STAMP, but has very low performance in IntegerSet.

Notwithstanding, 212orecs never provide a performance advantage over 216

orecs. The latter allow for at least 10% more performance on basically all benchmarks, they are less prone to slowdowns associated with certain Shift val- ues (especially with Mult32 on IntegerSet and Mult on STAMP), and their space overhead of 512KB is probably still practical for many applications. Therefore, I will not consider 212orecs further.

14 16 18 20 22 24 26 28 30 32 34 36 3 4 5 6 8 10 Genome, 12 threads Execution time (s) 10 15 20 25 30 35 40 45 50 55 60 3 4 5 6 8 10 Vacation-Hi, 12 threads 2 2.5 3 3.5 4 4.5 5 5.5 6 6.5 7 3 4 5 6 8 10 KMeans-Hi, 12 threads Execution time (s) 3.4 3.6 3.8 4 4.2 4.4 4.6 4.8 5 5.2 5.4 5.6 3 4 5 6 8 10 KMeans-Hi, 4 threads 5 6 7 8 9 10 11 12 3 4 5 6 8 10 RBTree-Large, 12 threads Throughput (tx/ µ s) 4 5 6 7 8 9 10 11 12 3 4 5 6 8 10 RBTree-Small, 12 threads 3 3.5 4 4.5 5 5.5 6 6.5 7 3 4 5 6 8 10 SkipList-Large, 12 threads Throughput (tx/ µ s) Shift 1 1.2 1.4 1.6 1.8 2 2.2 2.4 2.6 2.8 3 3 4 5 6 8 10 LinkedList-Large, 12 threads Shift LSA-Simple LSA-Mult LSA-Mult32

Figure 5.4: Performance of LSA with different hash functions for 212orecs and

For the Shift parameter, we do not need to consider a value of 10 because it never provides a performance advantage except a very small one in Vacation with a larger number of orecs, it does not perform well in many of the IntegerSet benchmarks, and it performs very poorly in KMeans. Likewise, we can discard a Shift value of 3: It is never beneficial with Mult32, performs badly with Simple in IntegerSet, and only provides a slight benefit in a few other benchmark configurations (e. g., Simple on four-thread RBTree-Large, or KMeans-Hi with 24 threads).

After this pruning of certain parameter values, we can consider just a Shift parameter ranging from 4 to 8 and 216to 222 orecs. Because we do not want to

assume automatic tuning and want to find a general-purpose memory–to–orec mapping, we need to select one set of parameter values for each hash function that provides good performance across all benchmarks.

Figures 5.5 and 5.6 show the performance of LSA with the three hash func- tions on selected STAMP benchmark configurations. Genome benefits from a large number of orecs, especially when Simple is used. Vacation-Lo and