MESI--CPU缓存一致性协议浅谈

CPU为何要有高速缓存

现代处理器为了提高访问数据的效率,在每个CPU核心上都会有多级容量小,速度快的缓存(分别称之为L1 cache,L2 cache,多核心共享L3 cache等),用于缓存常用的数据。由于内存的速度要比CPU慢将近100倍,数据被修改时也只是先更新cache,并不是直接写回到主存中(CPU是不能直接跟内存进行通信的,CPU只对接cache,然后由cache对接主存),这样一来就造成了缓存中的数据与内存不一致。如果系统是单核处理器,所有线程看到的都是缓存中的最新数据,当然没有问题。但如果系统是多核处理器,同一份主存数据可能会被缓存到多个核心 cache中,在运行中只要其中一个核心修改了缓存中的值,如果其他CPU核心没有得到及时的通知,就会造成缓存不一致的情况,影响系统的运行结果。MESI协议的出现,就是为了解决多核处理器时代,缓存不一致的问题的。

带有高速缓存的CPU执行计算的流程

1程序以及数据被加载到主内存

2指令和数据被加载到CPU的高速缓存

3CPU执行指令,把结果写到高速缓存

4高速缓存中的数据写回主内存

 

目前流行的多级缓存结构

 

由于CPU的运算速度超越了1级缓存的数据I\O能力,CPU厂商又引入了多级的缓存结构。

多级缓存结构

win10系统,可以到任务管理器中查看:

 

多核CPU多级缓存一致性协议MESI

多核CPU的情况下有多个一级缓存,如何保证缓存内部数据的一致,不让系统数据混乱。这里就引出了一个一致性的协议MESI。

 

MESI协议缓存状态

MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:

缓存行(Cache line):缓存存储数据的单元。

 

 

注意:
对于M和E状态而言总是精确的,他们在和该缓存行的真正状态是一致的,而S状态可能是非一致的。如果一个缓存将处于S状态的缓存行作废了,而另一个缓存实际上可能已经独享了该缓存行,但是该缓存却不会将该缓存行升迁为E状态,这是因为其它缓存不会广播他们作废掉该缓存行的通知,同样由于缓存并没有保存该缓存行的copy的数量,因此(即使有这种通知)也没有办法确定自己是否已经独享了该缓存行。

从上面的意义看来E状态是一种投机性的优化:如果一个CPU想修改一个处于S状态的缓存行,总线事务需要将所有该缓存行的copy变成invalid状态,而修改E状态的缓存不需要使用总线事务。

现代CPU的数据一致性实现 = 缓存锁(MESI ...) + 总线锁

位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题

缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。最常见的缓存行大小是64个字节。当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

 

图1说明了伪共享的问题。在核心1上运行的线程想更新变量X,同时核心2上的线程想要更新变量Y。不幸的是,这两个变量在同一个缓存行中。每个线程都要去竞争缓存行的所有权来更新变量。如果核心1获得了所有权,缓存子系统将会使核心2中对应的缓存行失效。当核心2获得了所有权然后执行更新操作,核心1就要使自己对应的缓存行失效。这会来来回回的经过L3缓存,大大影响了性能。如果互相竞争的核心位于不同的插槽,就要额外横跨插槽连接,问题可能更加严重。

 

案例:

 

 

public class T_CacheLinePadding {

 

    private static class T {

        public volatile long x = 0L;

    }

 

    public static T[] arr = new T[2];

 

    static {

        arr[0] = new T();

        arr[1] = new T();

    }

 

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {

 

            for (long i = 0; i < 10000_0000L; i++) {

                arr[0].x = i;

            }

 

        });

 

        Thread t2 = new Thread(() -> {

 

            for (long i = 0; i < 10000_0000L; i++) {

                arr[1].x = i;

 

            }

 

        });

 

        final long start = System.nanoTime();

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println((System.nanoTime() - start) / 100_0000);

    }

 

}

执行结果:

优化后,不在同一个缓存行案例: 使用缓存行的对齐能够提高效率

 

 

public class T_CacheLinePadding2 {

 

    private static class Padding {

        public volatile long p1, p2, p3, p4, p5, p6, p7;

    }

 

    private static class T extends Padding {

        public volatile long x = 0L;

    }

 

    public static T[] arr = new T[2];

 

    static {

        arr[0] = new T();

        arr[1] = new T();

    }

 

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {

            for (long i = 0; i < 10000_0000L; i++) {

                arr[0].x = i;

            }

        });

 

        Thread t2 = new Thread(() -> {

            for (int i = 0; i < 10000_0000L; i++) {

                arr[1].x = i;

            }

        });

 

        final long start = System.nanoTime();

        t1.start();

        t2.start();

        t1.join();

        t2.join();

        System.out.println((System.nanoTime() - start) / 100_0000);

    }

 

}

执行结果:

 

 

猜你喜欢

转载自blog.csdn.net/huzhiliayanghao/article/details/106872909