Clang在跨平台方面比GCC更具确定性吗?

7
我正在考虑使用C++编写一个多用户实时策略游戏(部分)。我很快发现,一个硬性要求是游戏模拟必须在服务器和所有客户端上完全确定,以便将网络通信限制在用户输入而不是游戏状态本身。由于每个人都有不同的电脑,这似乎是一个难题。
那么,有没有一种“神奇”的方法可以让C++编译器创建一个可执行文件,在Linux(服务器)、Windows和Mac之间完全确定?我认为两个主要的开源C++编译器是GCC和Clang,所以我想知道其中一个在这方面表现更好。
我还对任何可用于验证C++确定性的测试套件感兴趣。
[编辑] 通过确定性,我指的是编译后的程序,在相同的初始状态和相同顺序的输入下,将始终在任何平台上产生相同的输出,包括网络上的输出。对我来说,“一致”听起来是这种行为的适当定义,但我不是母语为英语的人,所以我可能会误解确切的含义。
[编辑#2] 虽然关于确定性/一致性是否重要,我是否应该在游戏引擎中追求它,以及在C++中它通常有多大问题的讨论非常有趣,但它实际上并没有回答问题。到目前为止,没有人告诉我是否应该使用Clang或GCC来获得最可靠/确定性/一致性的结果。
[编辑#3] 我突然想到有一种方法可以在C++中获得与Java完全相同的结果。必须获取JVM的开源实现,并提取实现运算符和数学函数的代码。然后将其转换为一个独立的库,并调用其中的内联函数,而不是直接使用运算符。手动完成这项工作会很麻烦,但如果代码是生成的,则是一个完美的解决方案。甚至可能可以使用类和运算符重载来完成,使其看起来自然。

3
“确定性”的定义是:只要您不依赖于外部来源(用户输入,内存分配器地址等)来确定控制流程,也不涉及任何未定义、未指定或实现定义的行为,那么C++程序应该始终运行相同。 - Oliver Charlesworth
@Oli:如果调用C标准库中的某些函数,尤其是rand()(而且大多数游戏都具有一定的随机性),那么情况就不是这样了。 - David Hammen
我认为OP更关心在各个平台上的一致性。例如,知道数据类型具有相同的大小,整数操作以相同的方式溢出,甚至结构体具有相同的内存布局。 - thkala
@thkala:不,他在谈论锁死式网络,每个客户端都运行游戏管理代码,这样你只需要在机器之间传递控制器数据。为使其正常工作,每台机器必须执行与其他机器完全相同的计算。因此,每个程序的游戏循环必须是确定性的:给定相同的输入,它提供二进制相同的输出。 - Nicol Bolas
3
@Sebastien:确实。但通常情况下,在编写代码时不应依赖于任何特定的行为方式。尤其是,如果你依赖于未定义的行为(例如通过负数移位),那么即使在同一台机器上使用相同的编译器,您可能也无法获得相同的行为。如果您依赖于实现定义的行为(例如对负数进行右移),则需要确保您使用的编译器和平台都执行相同的操作(它们在文档中声明了其行为)。 - Oliver Charlesworth
显示剩余3条评论
5个回答

0
由于每个人都有不同的电脑,这似乎是一个难题。
其实不是。实际上,这种网络编程非常简单,只要不违反规范的定义。IEEE-754非常清楚地规定了浮点数运算、舍入等具体细节,并在各个平台上实现完全相同。
你需要注意的最重要的一点是,在需要确定性的代码中不要依赖于SIMD CPU指令(注:这涉及物理、人工智能等方面的游戏状态,而不是图形,图形需要使用SIMD)。这些指令对浮点数规则采取了快速和松散的处理方式。因此,在你的游戏代码中不能使用SIMD;只能在“客户端”代码中使用(图形、声音等)。
此外,你需要确保你的游戏状态不依赖于时间等因素;每个游戏状态时钟周期应该是一个固定的时间间隔,而不是基于PC的时钟或任何类似的因素。
显然,你应该避免使用你没有代码的任何随机函数。但同样要注意:只针对主要的游戏循环;图形等内容可以是客户端特定的,因为它们只是视觉效果,不影响游戏本身。

这基本上就是保持两个游戏状态同步的全部内容了。你使用的编译器不会成为大问题。

请注意,星际争霸和星际争霸II使用这作为它们的网络模型基础。它们都可以在Mac和PC上运行,并且都可以相互对战。因此很有可能,而且不需要Clang。

虽然如果你喜欢Clang,你应该使用它。但那应该是因为你喜欢它,而不是为了网络。


例如,三角函数/超越函数如 sinlog 呢? - Oliver Charlesworth
假设他只使用单一的编译器和标准库实现(跨多个平台),那应该不会有问题。但为了增加安全性,他可以实现这些函数的通用版本。 - Nicol Bolas
4
编译器使用比IEEE-754要求更高的内部精度寄存器后,会导致错误,你将获得所有规范内的结果,但不再是位完全相同的结果。这个问题在10年前破坏了几个网络游戏,因为为英特尔和AMD优化的代码路径有稍微不同的计算顺序,非常接近但不完全相同。这仍然是一个巨大的问题。 - Nils Pipenbrinck
1
@Oli: 没错。rand()是最糟糕的罪犯。sin()和log()也不一致,直到最后一位。同样,即使像a*(b+c)a+b+c这样无害的东西也可能是罪犯。结果在同一台计算机、同一种编译器,但不同的优化级别下可能会有所不同。浮点运算既不是结合律也不是分配律。 - David Hammen

0
不要依赖未定义或未指定的行为(特别是不要使用浮点数),无论你使用哪种编译器都不重要。
如果a是1,b是2,则a + b等于3。这是语言标准保证的。
C++不是一个在某些编译器中有些事情是'确定性',而在其他编译器中它们并不是的国度。C++声明了一些事实(比如1+2==3),并留下一些事情给编译器处理(比如函数参数的求值顺序)。如果你的程序的输出只依赖于前者(以及用户输入),并且你使用符合标准的编译器,那么你的程序将始终给出相同的输出,只要用户输入相同。
如果你的程序的输出还取决于(比如)用户的操作系统,那么程序仍然是确定性的,只是输出现在由用户的输入和他们的操作系统来确定。如果你希望输出仅依赖于用户的输入,那么你需要确保用户的操作系统不是程序输出的一个因素。一种方法是只依赖于语言标准所保证的行为,并使用符合该标准的编译器。
总之,所有的代码都是基于其输入的确定性。你只需要确保输入仅由你想要的内容组成即可。

-1

我认为你使用的编译器并不是很重要。

例如,Doom就使用了完全确定性的方法。他们使用一个固定的“随机”数字数组来替换随机数生成器。游戏时间按照游戏中的滴答声(如果我没记错的话,大约为1/30秒)进行计量。

如果您通过游戏机制来衡量所有事物,而不是将工作转移至各种版本可能存在的标准库,那么我认为您应该能够在不同的计算机上实现良好的可移植性。当然,前提是这些机器运行您的代码足够快!

但是,网络通信本身可能会产生问题:延迟、丢包等。您的游戏应该能够处理延迟的消息,并在必要时重新同步自己。例如,您可能想定期发送完整(或至少更详细)的游戏状态,而不仅仅依赖于用户输入。

还要考虑可能的漏洞:

  • 客户端:我扔了一枚手雷
  • 服务器:你没有手雷
  • 客户端:我不在乎。我还是扔了一枚手雷

该网络模型的目的在于确保最大的性能。除了检测到完全不同步(从而中断该模型)之外,在任何时刻发送整个游戏状态都与此目标背道而驰。另外,您无法通过利用程序“扔手榴弹”。客户端仅发送控制器数据,而不是数据背后的含义。因此,每个客户端都会看到您按下“K”键,并根据当前状态进行解释。如果您黑进游戏以允许“K”始终扔手榴弹,则其他客户端将没有此功能。因此,不同步将很快发生。 - Nicol Bolas

-1

这有点像一个愚蠢的任务。在大端机器和小端机器上,或者在64位机器、32位机器和其他随机机器上,你的程序不会是“完全确定性的”(无论这意味着什么),也不会到最后一位。

说到随机性,许多游戏都有随机元素。如果你通过调用C标准函数rand()来实现这一点,那么一切都不确定。


一些最基本的研究表明,许多即时战略游戏之所以能够运行,是因为它们是完全确定性的。一旦服务器检测到客户端发生偏差,客户端就会被踢出。因此,这不仅是一个可以实现的目标,而且是大多数即时战略游戏设计文档都遵循的原则。而且,大多数严肃的游戏开发都依赖于自制的rand()函数,也正是因为如此。 - Sebastien Diot
CPU中没有真正的随机性,它们是确定性的。具有相同种子的伪随机数生成器会产生相同的值序列。当然,由于标准不要求任何特定的伪随机数生成器算法,因此您需要实现自己的PRNG或确保在每个平台上使用相同版本的相同stdlib实现。 - user395760

-1

如果你开始使用浮点数,那么你的所有打赌都是错的。你会遇到难以发现/修复的问题,即使在同一平台上,仅通过选择Intel或AMD CPU就会得到不同的值。

许多运行时库都针对不同的芯片优化了代码路径。这些都在规范范围内,但有些比其他一些稍微精确一些。这导致微小的舍入误差,早晚会累积成可能破坏事物的差异。

你的目标应该是尽量避免100%的确定性。毕竟:如果对手比应该向左多一个像素,对于玩家来说是否重要?并不重要。重要的是,客户端和服务器之间的小差异不会破坏游戏玩法。

玩家在屏幕上看到的东西应该是确定性的,这样他就不会感到被欺骗,但这不是必需的。

我所参与开发的游戏通过不断地重新同步所有实体的游戏状态来实现这一点。但我们几乎从未发送整个游戏状态,而是每帧发送少量对象的游戏状态,在几秒钟内分配工作。

只需给最重要的物体更高的优先级,而不是其他物体,就可以了。例如,在赛车游戏中,如果对手车距离你很远,那么确切的位置并不重要,每20秒更新一次即可。

在这些更新之间,相信小的舍入误差不会积累太多,以至于会遇到麻烦。


这并不适用于所有游戏。我考虑的是类似Minecraft的游戏。我们谈论的是一些“方块”,甚至无法用64位长整型表示。这种状态无法在常规间隔内同步,因为它太大了。它必须在本地重新计算,并且在每个客户端上以完全相同的方式重新计算。 - Sebastien Diot
确定性可能是稳定软件的一个好目标,可重复性。当然,确保不同平台给出相同的浮点结果可能很困难,例如,浮点SIMD指令可能使用不同的内部舍入;但是,在我看来,引擎应该能够以精确确定的方式运行其核心状态,这并没有什么问题-在某些情况下,这甚至可能是控制器感觉的关键。可以将浮点值包装起来,以确保始终使用所需的方法。可以使用软件浮点实现进行测试? - centaurian_slug

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接