x86上Java的最小侵入式编译障碍

时间:2013-02-02 09:37:14

标签: java performance memory x86 barrier

如果我通过共享的ByteBuffer或类似工具来处理与其他进程交互的Java进程,那么C / C ++中编译器障碍的最小侵入性是什么?不需要可移植性 - 我对x86特别感兴趣。

例如,我有2个进程根据伪代码读取和写入内存区域:

p1:
    i = 0
    while true:
      A = 0
      //Write to B
      A = ++i

p2:
    a1 = A
    //Read from B
    a2 = A

    if a1 == a2 and a1 != 0:
      //Read was valid

由于x86上的严格内存排序(加载到不重新排序的单独位置和读取到未重新排序的单独位置),这不需要C ++中的任何内存屏障,只是每次写入之间和每次读取之间的编译屏障(即asm)易失性)。

如何以最便宜的方式在Java中实现相同的排序约束。是否有什么比写一个易变的东西更少侵入?

2 个答案:

答案 0 :(得分:4)

您可以使用lazySet,它可以比设置volatile字段快10倍,因为它不会使CPU管道停顿。例如如果需要,可以使用AtomicLong lazySet或不安全的等效项。

答案 1 :(得分:4)

sun.misc.Unsafe.putOrdered应该做你想做的事 - 一个带有锁定的商店,在x86上由volatile表示。我相信编译器不会在它周围移动指令。

这与AtomicInteger和朋友的lazySet相同,但不能直接与ByteBuffer一起使用。

volatileAtomicThings类不同,该方法适用于您使用它的特定写入,而不适用于成员的定义,因此使用它并不意味着读取任何内容。 / p>

看起来你正在尝试实现seqlock之类的东西 - 意味着你需要避免在版本计数器的读取A之间重新排序,以及数据本身的读/写。普通的int不会削减它 - 因为JIT可能会做各种顽皮的事情。我的建议是为你的计数器使用volatile int,然后用putOrdered将其写入。这样,你不需要为易失性写入(通常是十几个或更多周期)付出代价,同时获得易失性读取隐含的编译器障碍(这些读取的硬件障碍是无操作,使它们快速)。

所有这一切,我认为你在这里是一个灰色区域,因为lazySet不是正式记忆模型的一部分,并且不能完全适应之前发生的事情,所以你需要更深入地了解实际的JIT和硬件实现,看看你是否可以用这种方式组合。

最后,即使使用易失性读取和写入(忽略lazySet),我也不认为你的seqlock从java内存模型的角度来看是合理的,因为易失性写入只会在之前发生写入并稍后读取另一个线程,以及写入线程中的早期操作,但不会在写入线程上写入之后的读取和操作之间读取。换句话说,它是单向栅栏,而不是双向栅栏。我相信读取线程可以看到N + 1版本到共享区域的写入,即使它读取A == N两次。

评论澄清:

易失性只会形成单向障碍。它与某些API中WinTel使用的获取/释放语义非常相似。例如,假设A,Bv和C最初都为零:

Thread 1:
A = 1;   // 1
Bv = 1;  // 2
C = 1;   // 3

Thread 2:

int c = C;  // 4
int b = Bv; // 5
int a = A;  // 6

这里,只有Bv是易变的。这两个线程在概念上对你的seqlock编写者和读者做了类似的事情 - 线程1在一个顺序中写入一些东西,而线程2以相反的顺序读取相同的东西,并尝试从中推断排序。

如果第二个线程的b == 1,那么a == 1总是,因为1发生在2之前(程序顺序),5发生在6之前(程序顺序),最关键的是2发生在5之前5写在2的值。因此,这样写入和读取Bv就像一个栅栏。上面的事情(2)不能“移动到下面”(2),下面的事情(5)不能“移动到”5以上。注意我只限制每个线程直接移动一个,但不是两个,这将我们带到下一个例如:

与上述相同,你可以假设如果a == 0,那么c == 0也是如此,因为C是在a之后写的,之前是先读过的。然而,挥发物并不能保证这一点。特别是,上述发生之前的推理并不能防止(3)被线程2观察到移动到(2)以上,也不会阻止(4)被推到下面(5)。

更新

让我们具体看看你的例子。

我认为可能会发生这种情况,展开在p1中发生的写循环。

<强> P1:

i = 0
A = 0
// (p1-1) write data1 to B
A = ++i;  // (p1-2) 1 assigned to A

A=0  // (p1-3)
// (p1-4) write data2 to B
A = ++i;  // (p1-5) 2 assigned to A

<强> P2:

a1 = A // (p2-1)
//Read from B // (p2-2)
a2 = A // (p2-3)

if a1 == a2 and a1 != 0:

假设p2为a1和a2看1。这意味着在p2-1和p1-2(以及扩展p1-1)之间以及p2-3和p1-2之间发生了一种情况。然而,在p2和p1-4之间发生了任何事情。所以实际上,我相信在p2-2处读取B可以观察到p1-4处的第二个(可能是部分完成的)读取,它可以“移动到p1-2和p1-3上的易失性写入”。

有趣的是,我认为你可能仅就这一点提出一个新问题 - 忘记更快的障碍 - 即使是在波动的情况下这是否仍然有用?