verilog中数组的索引顺序啥意思?如[N:1]
Posted
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了verilog中数组的索引顺序啥意思?如[N:1]相关的知识,希望对你有一定的参考价值。
定义一个寄存器标量数组时,有这么些写法:
reg q[5:0];
reg q[0:5];
reg q[5:-1];
等等,大数放在前面和后面有什么不同?为什么还可以用负数?
“发布顺序”是啥意思?
【中文标题】“发布顺序”是啥意思?【英文标题】:What does "release sequence" mean?“发布顺序”是什么意思? 【发布时间】:2016-11-28 16:07:03 【问题描述】:我不明白,如果我们在下面的示例中有 2 个线程,为什么没有 release sequence
会出现问题。我们对原子变量 count
只有 2 个操作。 count
依次递减,如输出所示。
来自 Antony Williams 的 C++ Concurrency in Action:
我提到你可以在
store
到一个原子变量和来自另一个线程的那个原子变量的load
之间得到一个synchronizes-with relationship
,即使在store
之间有一系列read-modify-write
操作。和load
,前提是所有操作都经过适当标记。如果存储标记为memory_order_release
、memory_order_acq_rel
或memory_order_seq_cst
,并且加载标记为memory_order_consume
、memory_order_acquire
或memory_order_acquire
或memory_order_seq_cst
,则链中的每个操作都加载由之前的操作,那么操作链构成一个发布序列和初始存储synchronizes-with
(对于memory_order_acquire
或memory_order_seq_cst
)或者是dependency-ordered-before
(对于memory_order_consume
) 最终加载。链中的任何原子读取-修改-写入操作都可以具有任何内存顺序(甚至是memory_order_relaxed
)。要了解这意味着什么(发布顺序)以及它为何如此重要,请考虑将
atomic<int>
用作共享队列中项目数的计数,如下面的清单所示。处理事情的一种方法是让生成数据的线程将项目存储在共享缓冲区中,然后执行
count.store(number_of_items, memory_order_release)
#1 让其他线程知道数据可用。然后,在实际读取共享缓冲区 #4 之前,使用队列项目的线程可能会执行count.fetch_sub(1,memory_ order_acquire)
#2 从队列中声明项目。一旦计数变为零,就没有更多的项目了,线程必须等待#3。
#include <atomic>
#include <thread>
#include <vector>
#include <iostream>
#include <mutex>
std::vector<int> queue_data;
std::atomic<int> count;
std::mutex m;
void process(int i)
std::lock_guard<std::mutex> lock(m);
std::cout << "id " << std::this_thread::get_id() << ": " << i << std::endl;
void populate_queue()
unsigned const number_of_items = 20;
queue_data.clear();
for (unsigned i = 0;i<number_of_items;++i)
queue_data.push_back(i);
count.store(number_of_items, std::memory_order_release); //#1 The initial store
void consume_queue_items()
while (true)
int item_index;
if ((item_index = count.fetch_sub(1, std::memory_order_acquire)) <= 0) //#2 An RMW operation
std::this_thread::sleep_for(std::chrono::milliseconds(500)); //#3
continue;
process(queue_data[item_index - 1]); //#4 Reading queue_data is safe
int main()
std::thread a(populate_queue);
std::thread b(consume_queue_items);
std::thread c(consume_queue_items);
a.join();
b.join();
c.join();
输出(VS2015):
id 6836: 19
id 6836: 18
id 6836: 17
id 6836: 16
id 6836: 14
id 6836: 13
id 6836: 12
id 6836: 11
id 6836: 10
id 6836: 9
id 6836: 8
id 13740: 15
id 13740: 6
id 13740: 5
id 13740: 4
id 13740: 3
id 13740: 2
id 13740: 1
id 13740: 0
id 6836: 7
如果有一个消费者线程,这很好;
fetch_sub()
是读取,具有memory_order_acquire
语义,而存储具有memory_order_release
语义,因此存储与加载同步,线程可以从缓冲区中读取项目。如果有两个线程读取,第二个
fetch_sub()
将看到第一个写入的值,而不是存储写入的值。如果没有关于release sequence
的规则,第二个线程不会有happens-before relationship
和第一个线程,并且读取共享缓冲区是不安全的,除非第一个fetch_sub()
也有memory_order_release
语义,这将在两个消费者线程之间引入不必要的同步。如果没有release sequence
规则或fetch_sub
操作上的memory_order_release
,则无需要求queue_data
的存储对第二个消费者可见,并且您将发生数据竞争。
他是什么意思?两个线程都应该看到count
的值是20
?但在我的输出中,count
在线程中依次递减。
谢天谢地,第一个
fetch_sub()
确实参与了发布序列,因此store()
与第二个fetch_sub()
同步。两个消费者线程之间仍然没有同步关系。如图 5.7 所示。图5.7中虚线表示释放顺序,实线表示happens-before relationships
【问题讨论】:
真正的问题是什么?为什么 std 不只是说 acq 读取与曾经发生的所有 rel 存储同步? 【参考方案1】:这意味着初始存储与最终加载同步即使最终加载读取的值与开始时存储的值不直接相同,但它是修改后的值可以进入的原子指令之一。一个更简单的例子,假设有 3 个线程竞速执行这些指令(假设 x 在竞速前初始化为 0)
// Thread 1:
A;
x.store(2, memory_order_release);
// Thread 2:
B;
int n = x.fetch_add(1, memory_order_relaxed);
C;
// Thread 3:
int m = x.load(memory_order_acquire);
D;
根据比赛的可能结果,n
和 m
的可能值是什么?根据我们在m
和n
上阅读的内容,我们对指令A
、B
、C
和D
的排序有什么保证?
对于n
,我们有两种情况,0
或2
。对于m
,我们可以读取0
、1
、2
和3
。
两者有六种有效组合。让我们看看每个案例:
m = 0, n = 0
。我们没有任何 synchronizes-with 关系,因此我们无法推断出任何 happens-before 关系,除了明显的B
happens-before C
m = 0, n = 2
。尽管fetch_add
操作读取了store
写入的值,但由于fetch_add
具有relaxed
内存排序,因此两条指令之间没有同步 关系。我们不能说 A
happens-before C
m = 1, n = 0
。与以前类似,由于fetch_add
没有release
语义,我们无法推断fetch_add
和load
操作之间的synchronizes-with 关系,因此我们没有'不知道B
之前发生过 D
m = 2, n = 0
。我们用acquire
语义load
读取的值已经用release
语义store
写入。我们保证store
同步 load
,因此A
happens-before D
m = 2, n = 2
。与上面相同,store
同步 load
,因此A
happens-before D
。像往常一样,从 fetch_add
读取的值与线程 1 中的 store
d 相同这一事实并不意味着任何同步关系。
m = 3, n = 2
。在这种情况下load
读取的数据已经被fetch_add
写入,fetch_add
读取的数据已经被store
写入。但是因为fetch_add
具有relaxed
语义,所以store
和fetch_add
之间以及fetch_add
和load
之间不能假设同步。显然,在这种情况下不能假设同步,与m = 0, n = 0
的情况相同。这是发布顺序概念派上用场的地方:线程1中的release
语义store
将同步线程中的acquire
语义load
3 只要正在读取的值已写入release sequence
,其中包括
-
稍后在与释放操作相同的线程中执行的所有存储
从同一个发布序列中读取值的所有原子读-修改-写操作。
在这种情况下,由于fetch_add
是原子读取-修改-写入操作,我们知道线程 1 中的 store
同步线程 3 中的 load
,因此 @ 987654391@ 之前发生 D
。不过,我们仍然不能说B
和C
的排序。
在你的情况下,你有这个伪代码,假设number_of_items = 2
:
// Thread 1
Item[0] = ...;
Item[1] = ...;
count.store(2,memory_order_release);
// Thread 2
int i2 = 0;
while (i2 = count.fetch_sub(1,memory_order_acquire) <= 0 ) sleep();
auto x2 = Item[i2-1];
process(x2);
// Thread 3
int i3 = 0;
while (i3 = count.fetch_sub(1,memory_order_acquire) <= 0 ) sleep();
auto x3 = Item[i3-1];
process(x3);
假设读入i2
的第一个正值是2
,因此读入i3
的第一个正值是1
。由于从线程 2 读取的值已经从线程 1 中的存储区写入,因此存储区同步加载,我们知道来自线程 1 的 Item[1] = ...;
happens-before em> auto x2 = Item[1];
在线程 2 中。但是从线程 3 读取的值 1
已由线程 2 写入,fetch_sub
没有 release
语义。因此,来自线程 2 的 fetch_sub
不会同步来自线程 3 的 fetch_sub
,但是因为来自线程 2 的 fetch_sub
是发布链的一部分在线程 1 中的 store
中,线程 1 中的 store
也同步线程 3 中的 fetch_sub
,从中我们知道 Item[0] = ...;
发生在 auto x3 = Item[0];
【讨论】:
同意!该评论为我澄清了很多except for the obvious B happens-before C
你确定吗?在 Sutter 的 Atomic Weapons 中,他说 memory_order_relaxed 不会产生任何障碍,因此任何东西都可以上下流动。因此 B 不能保证在 C 之前发生?
@geranimo 同一线程中的操作始终保证按您编写它们的顺序按顺序排列。即使在 C++98 语义中也是如此。
@pqnet 这不是真的。编译器和 CPU 可以根据需要重新排序。只有在同一线程中具有依赖关系的指令才需要在它们的依赖关系被计算后发生。
@geranimo 规则 #1。它们是full expressions
,因为它们用分号分隔。复杂的规则适用于子表达式(如参数评估),除非在某些情况下,您不知道首先评估哪个子表达式。请注意,sequenced-before
和 happens-before
不保留英文字面意思,happens-before
意味着您可以依赖第一个表达式的副作用在第二个表达式中可观察到,与指令重新排序无关(这对于sequenced-before
说明只有在您没有观察到这些副作用时才会发生)【参考方案2】:
他是什么意思?两个线程都应该看到 count 的值是 20?但在我的输出中,线程数会依次递减。
不,他没有。对count
的所有修改都是原子的,因此两个读取器线程在给定代码中总是会看到不同的值。
他在谈论释放顺序规则的含义,即当给定线程执行release
存储时,其他多个线程随后执行同一位置的acquire
加载形成一个释放顺序,其中每个后续acquire
加载与存储线程有happens-before关系(即完成存储happens-before em> 负载)。这意味着读取器线程中的加载操作是与写入器线程的同步点,写入器中在存储之前的所有内存操作都必须完成并在其相应的加载完成时在读取器中可见。
他是说没有这条规则,只有第一个线程会因此同步到作者。因此,第二个线程在访问 queue
时会发生数据竞争(注意:not count
,无论如何它都受到原子访问的保护)。理论上,在count
上的store
之前发生的数据的内存操作只有在其对count
的加载操作之后才能被2 号读取器线程看到。发布顺序规则确保不会发生这种情况。
总而言之:发布顺序规则确保多个线程可以在单个存储上同步它们的负载。有问题的同步是对数据的内存访问其他,而不是被同步的实际原子变量(由于是原子的,无论如何都保证同步)。
在此添加注意事项:在大多数情况下,此类问题仅与那些对重新排序内存操作很放松的 CPU 架构有关。英特尔架构不是其中之一:它是强排序的,并且只有少数非常特殊的情况可以重新排序内存操作。这些细微差别大多仅在谈论其他架构时才相关,例如 ARM 和 PowerPC。
【讨论】:
He's saying that without this rule, only the first thread would be thus synchronised to the writer. The second thread would therefore have a data race in accessing queue.
你的意思是,如果没有release sequence
,第一个消费者线程会看到queue
的值1, 2, ..., 18, 19
,而第二个消费者可能会看到queue
的值只有 1, 2, 3
?
更一般地说,数据竞争意味着行为未定义。但是,是的,您的示例是假设可能的。您可以想象一个设置,其中运行在不同物理 CPU 和缓存上的读取器线程 2 将访问旧版本的 queue
,因为在没有发布顺序要求的情况下,其包含 queue
的缓存行未同步到另一个使用更新的queue
缓存。但正如我所说,这不可能在 Intel 上发生,只能在其他架构上发生
你能描述一下其他架构吗?编译器怎么可能让它变得不安全,尽可能地努力?
这与编译器无关,而是与 CPU 的动态内存访问行为有关。假设您有 3 个物理 CPU 和 3 个不同的 L1 缓存。 CPU1 是写入器,CPU2 和 CPU3 是读取器。当 CPU1 在其本地缓存中执行存储时,由于 fetch_sub 的 aquire-release 同步,CPU1 在加载之前更新了其等效的缓存行。但是,如果没有释放顺序规则,CPU1 对 CPU3 的缓存没有这种义务,因此 CPU3 最终会读取queue
的陈旧值。注意我不知道是否有任何 CPU 架构实际上是这样的,这是假设的。
对于任何使用栅栏的架构,编译器必须在发布时插入栅栏并获取共享原子对象(本地原子可能被优化)。那么是否有发布序列就无关紧要了。即使最后一次修改是由另一个线程(而不是 RMW)完成的,保证执行 rel ever 的所有线程的写入对执行 acq 的所有线程都是可见的,这在实践中似乎实际上是不可避免的,因为 -如果有 HB,即使形式语义另有规定。【参考方案3】:
我偶然发现了和你一样的问题。我以为我理解正确,然后他带着这个例子进来,只使用 std::memory_order_aquire。很难找到任何好的信息,但最后我找到了一些有用的资源。 我不知道的主要信息是一个简单的事实,无论给定什么内存顺序(甚至是 std::memory_order_relaxed),读-修改-写操作总是在最新/最新值上工作。这可确保您在示例中不会有两次相同的索引。操作的顺序仍然会混淆(所以你不知道哪个 fetch_sub 会在另一个之前发生)。
这是 anthony williams 本人的回答,他指出读-修改-写操作始终适用于最新值:Concurrency: Atomic and volatile in C++11 memory model
此外,有人询问 fetch_sub 与 shared_ptr 引用计数的结合。这里 anthony williams 也做出了回应,并通过重新排序 fetch_sub 使情况变得清晰: https://groups.google.com/a/isocpp.org/forum/#!topic/std-discussion/OHv-oNSuJuk
【讨论】:
【参考方案4】:fetch_sub
是读-修改-写操作。它以原子方式从内存地址读取值,通过提供的参数将其递减,然后将其写回内存地址。这一切都是原子发生的。
现在,每个原子操作都直接读取和写入内存地址。 CPU 不依赖缓存在寄存器或缓存行中的值来获得性能。它直接读取和写入内存地址,并防止其他 CPU 在那个时候这样做。
“普通”(==relaxed)原子性不提供的是重新排序。编译器和 CPU 都争先恐后地读写,以加快程序的执行速度。
看下面的例子:
atomic integer i
regular integer j
Thread A:
i <- 5
//do something else
i -> j
//make some decisions regarding j value.
Thread B:
i++
如果没有提供内存顺序,则允许编译器和 CPU 将代码转换为
Thread A:
i -> j
i <- 5
//do something else
//make some decisions regarding j value.
Thread B:
i++
这当然不是我们想要的。决策是错误的。
我们需要的是内存重新排序。
内存顺序获取:不要打乱内存访问之前 内存顺序释放:不要在之后
打乱内存访问回到问题:
fetch_sub
既是读取一个值,又是写入一个值。通过指定memory order acquire
,我们说“我只关心阅读之前发生的动作顺序”
通过指定memory order release
,我们说“我只关心在写作之后发生的操作顺序。
但是您确实关心之前和之后的内存访问!
如果你只有一个消费者线程,那么sub_fetch
不会影响任何人,因为生产者无论如何都使用普通的store
,而fetch_sub
的影响只对调用fetch_sub
的线程可见。在这种情况下,您只关心读数 - 读数为您提供当前和更新的索引。存储更新后的索引(比如说x-1
)之后发生的事情并不那么重要。
但由于有两个线程读取和写入counter
,因此线程A将意识到线程B写入是很重要的> 给计数器一个新值,线程 B 知道线程 A 正在读取计数器的值。反之亦然 - 线程 B 必须知道线程 A 写入 一个新值到 counter
并且线程 A 必须知道线程 B 即将 读取 一个值计数器
你需要两个保证——每个线程都声明它即将读取和写入共享计数器。您需要的内存顺序是std::memory_order_acquire_release
。
但是这个例子很棘手。生产者线程只是在counter
不管中存储一个新值,而不考虑之前的值。如果生产者线程每次推送新项目时都递增计数器 - 您必须在生产者和消费者线程中都使用std::memory_order_acquire_release
,即使你有一个消费者
【讨论】:
"你需要两个保证 - 每个线程都声明它即将读取和写入共享计数器。你需要的内存顺序是std::memory_order_acquire_release
"你的意思是,来自书不正确? Williams 仅使用 memory_order_acquire
和 memory_order_release
我相当确定同一线程中按程序顺序排序的指令也具有happen-before
关系,因此设置j
的值将是5
或6
(如果 i++
在 i<-5
和 j<-i
指令之间竞争)以上是关于verilog中数组的索引顺序啥意思?如[N:1]的主要内容,如果未能解决你的问题,请参考以下文章