@eric1989
2016-04-28T14:46:44.000000Z
字数 1409
阅读 807
CPU读取数据是以一行作为单位填充的,这个一行就称之为缓存行,英文cacheline。每个CPU的缓存行长度都不一定,目前比较流行的长度是64个字节。
不同的cpu之间通过缓存协议来保证数据的一致性。如果cpu0上面的缓存行和cpu1上的共同,则该缓存行处于共享状态。此时如果cpu0要修改数据,则会先要求cpu1上的缓存行无效。这是通过缓存行一致性协议来保证的。
程序在运行的过程中,必然需要载入数据。假设此时cpu0载入了一个对象,该对象有5个属性。这5个属性很大可能是在同一个缓存行。而此时cpu1也载入了该对象,并且修改了其中的一个属性。由于缓存行是整行失效,就会导致cpu0不得不重新再次载入该对象。对应在运行的情况中可能就是线程1只关心对象的前4个属性,然后线程2修改了对象的第5个属性,导致线程1的对象失效,不得不再次重新读取整个对象。影响了运行效率。
既然一个对象可能被多个线程修改,而每个线程中可能关心的属性区域都不同。那么只要让不同区域的属性不要在一个缓存行中就可以避免缓存行失效带来的整体读取的负面影响。基于这个认识,要做的其实就很简单了。通过在一个属性前后填充几个无用的属性,拉大这个属性前后的数据长度,保证这个属性自己独占一个缓存行。这样该属性的修改就不会影响到整个对象数据在cpu中的有效性。下面用图来解释这个过程。
情况一
属性1 | 属性2 | 属性3 | 属性4 | 属性5 | 属性6 | 属性7 | 属性8 |
---|---|---|---|---|---|---|---|
对象属性1 | 对象属性2 | 对象属性3 | 对象属性4 | 对象属性5 | 对象属性6 | 对象属性7 | 对象属性8 |
上面每一个属性均占8字节,假设cpu的缓存行长度是64字节长。此时cpu0,1读取了该对象数据。其中cpu0修改了属性1,这会导致cpu1上整个对象数据失效。而cpu1上的线程此时的任务其实并不关心属性1,但是数据却失效了,不得不再次读取。如果这种情况很多,性能效率就会很慢。
情况二
属性1 | 属性2 | 属性3 | 属性4 | 属性5 | 属性6 | 属性7 | 属性8 |
---|---|---|---|---|---|---|---|
填充属性1 | 填充属性2 | 填充属性3 | 填充属性4 | 对象属性1 | 填充属性5 | 填充属性6 | 填充属性7 |
对象属性2 | 对象属性3 | 对象属性4 | 对象属性5 | 对象属性6 | 对象属性6 | 对象属性8 | 空 |
上面通过无意义的填充属性,保证了对象属性1会在cpu中单独占据一个缓存行。这样,两个争用线程,一个关注属性1,一个关注剩余属性。那么修改属性1的线程的修改动作,不会关注剩余属性的线程的数据失效。
上面的做法可以参考下面的示例代码
class LeftPad
{
protected long p1, p2, p3, p4, p5, p6, p7;
}
class RealValue extends LeftPad
{
protected volatile long point = -1; // 前后都有7个元素填充,可以保证该核心变量独自在一个缓存行中
}
class RightPad extends RealValue
{
protected long p9, p10, p11, p12, p13, p14, p15;
}
public class CpuCachePadingValue extends RightPad
{}
缓存行填充,实际上是通过让一个属性独占一个缓存行来达到目的的。那么也就是说浪费了一些空间。这样的浪费也会导致一定的损耗。所以最好只是在激烈争用的对象中才使用这样的填充技术,否则很难确定是否能带来性能提升。