d算术混淆大论战
隐式转换易错
问题是如何说服沃尔特
我想要输出隐式转换的警告的编译时选项
我更希望,移植C到D时,可报错,而不是保留地雷代码.应该是这样的-vimplicit-conversions隐式开关.
C系外系统编程语言倾向于用显式转换.
第1个示例,在D中修复了.乘法示例有意思,其他在速度相关时,意义不大.
奇怪的是,没有更多signed -> unsigned间的转换,那才是个大坑.
对包装和非包装(溢出陷阱)算术运算,现代编程语言倾向于有单独的运算符或内置操作符.
抓此类错误与检查数组边界一样有用.处理器早晚要加缺失指令来加快速度.
C++已用-Wconversion修复了.
当然,如果用模板库,可能会收到不想要警告.因此,好语法消除内联警告,鼓励打开最大严格性,并在方便时可选的禁用它.内联静默警告是ts比js好的地方.
写模板时要注意,可基于有无符号限制模板参数.
有符号模运算有点麻烦,但我自己重载了.
C++中unsigned int(a)有模运算.但人们按值区间类型用它,而不是模运算用它.如果命名为模整,则比正 整好得多.
C++仍可启用符号溢出检查,而D中整操作都是模运算的.
C++中的模运算,问题在优化不够.因为优化映射到圆上比到直线上的计算要难得多.这是D应该解决的问题.
问题在,复杂表达式中,它避免了优化.
理论上,可按单独表达式计算溢出,并推测计算,溢出时切换到慢速路径,但这是高级方法而不是系统级方法.在低级编程中,程序员希望代码映射到机器指令,而不会膨胀.你希望透明映射.
要使溢出检查真正强制转换,需要带约束的更高级的类型系统,这样编译器可知道从堆中提取的整数可有哪些值.
模算术
处理器支持数组中高效模运算索引,并为此提供特殊指令,dsp也可以.
假定a[i%a.length]与a[i]速度一样,则可按模运算定义数组索引.则无越界.完美实现内存安全
正基本上是从size_t来.
至少在32位系统上,内存缓冲区在技术上可能跨越一半以上的地址空间.
处理内存缓冲区的API函数必须处理size_t这种难题.而64位则不存在.缓冲大小可为整,而不必丧失功能.
模算术数学特性.
a+b-c可转为a-c+b.但如果处理潜在的整数溢出陷阱,那么就不能安全重排表达式.a+b上溢?a-c下溢?
很早前就失去了透明度.允许编译器优化大部分表达式.乘法和移位替换了整除常数.函数是内联的,循环是展开和/或矢量化的.
现有编程已能抓算术溢出.
正->整似乎是为了优化.多个整导致模板的多实例,这无意义.sizeof返回的是正的整(size_t).
现代容器不必返回正.
也许:重大更改并提供编译器标志来获取旧行为吗?然后是另一个用于捕获溢出的编译器标志.
源代码或编译标志中的提示来控制优化.
更快一致的优化代码,与性能不均匀的代码生成之间存在很大差异.
硬件在一致性能方面也很糟糕,如浮点值接近零(非正规数)的计算,这在实时/音频程序员中非常不受欢迎.
在面向高级编程语言中一直抓溢出.C等落后了.
对低级编程,处理器速度和分支预测有吸引力.
现代语言最佳解决方案可能是:改进类型系统,来证明表达式不会溢出.默认检查正溢出,提供内联禁止选项.
不必优化所有路径,关键性能一般只在一小组函数中.
D中正=整转换太痛苦,
auto var = arr.length,然后做减法,突然就溢出,在64位,不必用正了,C++现在也对整大小提供cont.ssize().
我不信.D和Phobos的设计可大量防止与内存安全无关的错误,有时甚至以效率代价.例如,@safe不需要默认初化整数,初化是不必要的且速度较慢.必要时覆盖默认值.与默认检查边界一样,这是最好的设计.
用-fwrapv|clang -fwrapv可提升性能.
高级模板代码中,如果条件总是假,如果不删它们,那么将会增加代码大小.好的优化器鼓励你写更高级的代码.
这样,
if (x < x + 1) { ... }
行不?
C++20提供std::ssize(容器),返回整类型.
这样,可编写假定普通算术且也适用于旧容器的模板.小语言适合重大更改.
这是内部循环问题.与缓存一样,一旦达到了循环中推出CPU管道循环缓冲区的阈值,然后就很重要了.
当程序员不断受到打击时,就是个问题.
可删除许多影响小的单独优化,但删除的每个优化都会降低竞争力.
目前,大多数C/C++代码库都不是在性能关键函数中按高级方式编写的,但编译器变得"更智能",硬件更多样化,因而向性能代码中更高级别编程迈进.拥有硬件越多样化,高质量优化就越有价值,调整代码成本就越高.
在D中,用模算术,且不限制整数.会得到额外膨胀.
在重要地方缺少优化是有影响的.此时,我指出这在内部循环中影响力最大,但当前C/C++代码库往往不会在性能敏感函数中用高级编程.
问题在于:如果可避免,人们不想手动调整内部循环.
如果从相同输入中获得相同的输出/响应,那么未偏离规范.
因此,如果在整数算法上检查溢出,则:
for(int i=1; i<99999; i++){
int x = next_monotonically_increasing_int_with_no_sideffect();
if (x < x+i){...}
}
与下面相同:
int x;
for(int i=1; i<99999; i++){
x = next_monotonically_increasing_int_with_no_sideffect();
...
}
assert(x <= maximum_integer_value - 99998);
良好语言规范应只指定可观察行为的要求(包括内存和接口要求).
测试假条件,如果计算可推导出x的上个值,则可完全删除循环,仅保留最后断定.
如果性能很重要,可手动优化它.无论如何,手动优化是获得高性能代码的必要条件.反面是在@safe中有未定义行为.同样,有检查边界,在性能重要时,可关闭它.
与一般警告一样多的缺点,我们应该解决它.这些转换可能太常见,而无法彻底弃用它们.尽管如此,旧代码仍会继续编译,但对新代码,语言要明确支持显式转换.
我们甚至不应警告整提升.到处都是显式转换的代码,非常难看.但可警告无符号/有符号转换.隐式转换为相同符号的更大整数不是反模式,可以保留他们.
它还*导致*错误.重构代码且类型变化时,强制转换可能不会做预期事情,比如意外截断整数值.
D相对C的进步之一(因为运行良好,很大程度上是隐藏的)是只有在不丢失位时,才会自动转换整数为更小整数的值区间传播.
实际使用中,D比C差.使用byte和short类型时,这是不断烦恼根源.
值区间传播仅适用于单个表达式,过于保守,无法在实际代码中提供太多帮助.
int i;
byte b = i & 0xFF;
//上面byte=>ubyte
ubyte a, b, c;
a = b | c;
都编译过了.
未定义行为,C++选择了性能,你也可选择其他.
编译器拒绝了a=b+c.或许该模环绕算法?我知道b/c可能区间,因而不会溢出?编译器需要显式转换,为何碍事呢.
如果,改为uint,又可以了,不要求转为ulong,这不一致.整提升/32位特殊.
但如编译阶段抓错误,加两个正字节与正没啥区别,都可溢出.
其他现代编程语言可运行时抓算术溢出.并允许在代码性能关键部分选择退出这些检查.
b+c可能创建不适合正字节的值.
因为有隐式截断为字节的C漏洞.
与C的整提升规则一致.我们尽量做好.
运行时抓溢出有其他问题.VRP可以安全的隐式转换为字节.
int i;
ubyte _tmp = i & 0xFF;
byte b = _tmp;
这很有趣,允许它.
int a, b, c;
a = b + c;
这里同样,会产生不合适值,编译器接受它.
问题是不一致.整数类型行为取决于宽度,使语言难以学习,并使通用代码为窄整数添加特例,就像std.math.abs中:
static if (is(immutable Num == immutable short) || is(immutable Num == immutable byte))
return x >= 0 ? x : cast(Num) -int(x);
else
return x >= 0 ? x : -x;
即使编译器提供了比语言规范更多保证,仍应尽可能避免未定义行为.
模算术没用,它使情况更糟.正确删除条件比错误的反转它要好.
未定义行为并不比不想要的定义行为更差.
我不同意.至少通过溢出,可清楚推断正在发生的事情.
fun(aLongArray[x]);
x *= 0x10000;
如果数组够长,按你提倡编译器的语义可能会:
x不能溢出,所以乘前,最多为0x7FFF.
我知道aLongArr更长,所以可省略检查边界.
与上面相比,溢出问题要小得多.
我主张捕捉溢出,除非明确禁用它.还主张同时拥有模运算符和钳(紧固)运算符
抓溢出,类似GCC的-ftrapv,整溢出会崩溃.
不能在@safe代码中允许未定义行为,@safe中不能有溢出时未定义行为.
断言未溢出就可以了.用-release开关,与c++的int(整,非正)类似.但反面不是.下面是可行的:
import core.checkedint;
bool check;
auto x = mulu(a,b,check);
assert(!check);
不确定编译器是否会在发布模式下利用溢出是未定义行为.
@safe代码应允许未定义行为.让它实现定义.要求代码保证内存安全.
可由语言标准留给编译器,但编译器仍强加通用内存安全要求.
我用-O3测试了溢出,它并未删除"边界检查".因此,编译器可内存安全的调整优化.
实现定义的解决方案,存在任何更改都可能破坏内存安全的问题.
其他一些函数的内存安全性可能取决于具有溢出整数的@safe函数的正确行为.
你的意思是@信任代码,但要更具体.它实际上是溢出,包装也可以这样说.可能是@信任代码未考虑负数.
如果计算x时溢出,则限制x为位宽,而不是任意位,是有意义的.需要时,可进一步限制它.
当然,这仅与禁用捕获溢出的@safe代码相关.
实现定义即供应商必须记录语义.
"未定义行为"即供应商不需要/但要鼓励记录行为.这是在解决硬件有未定义行为时,C语言规范中引入的.C++编译器之间竞争使他们利用这一点来搞最硬核优化.
这并不难,大约两三句话.只要理解二进制补码算术.
必须努力理解二进制补码.一些崇高的尝试:
Java:禁止所有无符号类型.最终不得不按黑客方式将重新添加它.
Python:数字可以增长且不损失精度.但是,代码变慢.
Javascript:一切都是双精浮点值!导致各种问题.浮点更难.
添加abs(short)和abs(byte)是个巨大错误.这些函数不在C语言中是有道理的.
试图隐藏计算机整数运算及整提升的工作原理,会导致无尽的失望和不可避免的失败.
不,VRP会发出错误.
int i;
byte b = i.to!byte;
i = -129;
b = i.to!byte; // std.conv.ConvOverflowException
安全溢出,应该比下面的好
byte b = cast(byte)i;
运行时检查溢出来抓漏洞.好的优化编译器在i区间大致值已知道时,可消除漏洞.如:
void foobar(byte[] a)
{
foreach (i ; 0 .. a.length)
a[i] = (i % 37).to!byte;
}
命令:
$ gdc-12.0.1 -O3 -fno-weak-templates -c test.d && objdump -d test.o
乘法和移位代替了慢除法,条件分支仅用于比较i与数组长度..to!byte部分无成本,通过mov %al,(%rsi,%rcx,1)指令直接把字节写入目标数组.
理解二进制补码很烦,有摩擦.
D还可改进:
使64位整数(非正)"默认"地全面检查.
对用内部"假设"指令来用限制信息提供编译器的受限整数提供库类型.此类型将选择合适的限制整数的存储类型.
高速要求时,加些干净语法来禁用运行时检查.
如此,加ARC加上本地GC,可有竞争力.
D应改进高级编程,及从高级到系统级的转换能力.
细节主要用于非常低级的技巧和易出错的位操作.有个好标准库,这不应是经常需要的.此外,由于SIMD的可用性,我发现位技巧不实用.在SIMD前,我有时会用正位技巧来模拟SIMD(用于处理图像),但这是神秘的.我只想在创建高精度相量(振荡器)或按位向量对待浮点数的极少数情况下这样.大多数程序员不需要这些知识,他们只需要个好库.
无论如何,要求类似C的熟练程度是糟糕的策略,因为这使D程序员更容易过渡到C++!
D需要朝着简单的方向发展,这是它相对于C++和Rust可以获得的主要优势.
不,你可以丢弃它
struct Thing {
short a;
}
// 不同.
Thing calculate(int a, int b) {
return Thing(a + b);
}
当前规则要求,在构造函数调用中显式转换.然后,稍后,重构Thing为int.它仍会编译,仍然存在显式转换,现在砍掉位.
显式转换问题,一旦写入它们,很难撤销.cast代表有问题.
short a;
short b = a + 1;
你可能就需要一个了.是的,可能会截断进位.
另一方面,如果在某些类型的通用代码中存在整数,则可能会丢失精度.
合理折衷是允许隐式转换输入的最大类型.在字面上可应用VRP.即:
short a;
short b = a + 1;
时,检查输入
a = type short
1 = VRP下转为`字节/极(甚至)`.
最大类型?短.所以可隐式转换为short.然后跑VRP来进一步变小它:
byte c=(a&0x7e)+1;//好的,VRP可知道它仍适合那里,所以它变得更小了.
但由于最大的原始输入适合"short",即使可能会丢失一个进位,它允许输出变为"short".
另一方面:
ushort b = a + 65535 + 3;
不,编译器可常折叠该字面,VRP根据其值调整大小为"int",因此需要显式转换来确保不会丢失*实际*输入精度.
short a;
short b;
short c = a * b;
我会允许的.输入是a和b,它们都是短,所以让输出也隐式截断回短.就像int一样,是的,乘法产生一个高字,但它可能不适合,我不希望编译器烦我.
折衷方案会平衡合法的安全问题与意外丢失或重构更改(如重构为整数,现在输入类型增长,并且编译器可再次发出错误)与几乎无处不在的烦人转换.
移除大部分转换,使剩下转换更加突出,它们是潜在的问题.
d干C的整提升和转换的成功一样,零惊奇.
但也有区别.
C可隐式转换为更短的整数,D没有,翻译时必须用cast().
但此时,不知道强制转换类型的D代码是否比C代码更脆弱,因为更改类型时,C和D都收不到警告.
因此,此时检测整数问题的最佳方法是:
1.VRP不转换.
2.公平:D带强制转换,或C带隐式强制转换.
可"克服"D整数会很好,没有一劳永逸,今天人们仍然一直从C转换为D.这是目前状态,兼容C语义很有用.
甚至C++也不断引入新基本类型,以便为该语言的每个新版本提供更好的类型安全.例如,在C++中std::byte不是算术类型.
C是在PDP-11上开发的,并且由于-11指令工作方式,而产生了整提升规则.float=>double提升规则同样如此.
多少人实际使用(并且需要)正?如果99%的用户不需要它们,归类为库类型就很好.
因为C是在-11上开发的.这已经延续到现代CPU中,考虑:
void tests(short* a, short* b, short* c) { *c = *a * *b; }
0F B7 07 movzx EAX,word ptr [RDI]
66 0F AF 06 imul AX,[RSI]
66 89 02 mov [RDX],AX
C3 ret
void testi(int* a, int* b, int* c) { *c = *a * *b; }
8B 07 mov EAX,[RDI]
0F AF 06 imul EAX,[RSI]
89 02 mov [RDX],EAX
C3 ret
你为使用短算术而不是整算术支付了3字节.它也比较慢.
一般,int应用于大多数计算,short和byte用于存储.
(现代CPU长期以来一直刻意优化和调整C语义.)
现代机器*肯定*了解C的工作原理.
正在只期望正值的API中非常有用,标记为uint表明预期,处理位掩码时,也很有用.对系统编程语言,也很重要.更反映现实.
需要库类型来操作位掩码会使D成为系统编程语言的一个完全笑话.
因而,尽量用整.
用户并不关心编译器如何执行指令.他们关心结果,如果赋值为字节,他们可能不在乎失去额外精度,不然,为何不用整?
不再,除在小整上可能较快.
1.你非常小心地演示了短算术,而不是与x86上的整算术大小相同的字节算术.
2.循环计数(或字节计数)不是明智的语言设计方法.可能与语言实现有关;整个程序性能可能与语言设计有关;但变化是微不足道的,不应妨碍正确的语义.
3.你代码示例完全按你建议一样,用短算术存储.此时,用短算术而不是整算术,会产生相同结果及更小代码.
4.(上接3)在更大,更有趣表达式中,不管语言语义,编译器一般都会自由地使用整作为临时变量.
较大代码大小肯定会给指令缓存带来更大压力,但减速不大.利用SIMD指令的自动向量代码看起来有点不同.
处理大型数组时,性能确实提高了很多.并且16位版本大约比32位版本快两倍(因为每个128位XMM寄存器代表8个短或4个整数).
如果希望D语言对SIMD友好,那么可以鼓励局部变量使用short和byte.
int a = int.max;
long b = a + 1;
writeln(b > 0); // 假
是的,我希望编译器确定是否需要溢出,并基于此生成适当指令.
对于int->long不经常出现原因是:因为a)不常转换int到long,且b)很少见整溢出.
正如我之前观察到的,没有解决方案.这是不同问题.最好坚持使用已充分理解问题,并且最适合常见CPU架构的机制.
代码大小的损失仍然存在
字节算术的代价是缺少寄存器.通用方案中,短与字节没啥区别.
如果客户期望有性能系统编程语言,就不行了.
还记得最近处理x87,dmd保持额外精度,来避免双精圆整问题?我传播给dmc,它让我赢得了设计胜利.客户在"浮点"算术上,基准测试,并宣布dmc慢了10%.双精圆整问题,他不感兴趣.
加载指令仍用额外操作数大小来覆盖字节.
加载短,会产生额外字节.
:不管语义,编译器用整作临时.
根据优化表达式方式,你会得到其他截断问题.x87则更慢.
我在这呆了40年了,没有神奇的解决方式,整提升是最实用的解决方案.最好花些时间学习它们,会没事的.
SIMD是它自己的世界,为什么D将向量类型作为核心语言特性?我不相信自动矢量化.
有趣的是,当不可用有符号除法指令时,除有符号是:保存操作数的符号,取反为无符号,除无符号,再取反.
无符号操作是CPU工作方式的核心,有符号依赖它.
重申:
C的规则:整提升,允许隐式向小转换.
D的规则:整提升,除非VRP通过,否则禁止隐式向小转换.
我提出规则:整提升,除非VRP通过或请求转换与最大输入类型相同(除非值明显越界,排除字面).禁止隐式向小转换.
实际计算不变.只是放宽D当前严格的隐式转换规则到更接近C许可标准.
codegen基本不变.中间值不变.类似C,但仅允许隐式转换回输入.
64位上,字节与字寄存器一样多.(从技术上讲,但应不惜一切代价避免使用高半寄存器.)
如果客户想要生成整,则生成整.
如何不用操作数大小覆盖前缀存储短?
我指的是乘法.可加载第二个寄存器,执行32位乘法,然后存储截断结果.在不同环境,这可能是值得的.
ubyte x,y,z,w; w = x + y + z.
(((x+y)%2^32%2^8)+z)%2^32%2^8
//上下是一样的.
(((x+y)%2^32)+z)%2^32%2^8
2^32在32位寄存器上隐式用的.2^8是隐式截断.
前者,有两个显式截断,可重写为后者,去掉中间截断,得到与提升完全相同结果.
访问这些字节寄存器需要额外的REX字节.
他们需要为子表达式插入强制转换到整中.这不好.
考虑比加载和存储更复杂的表达式.
考虑:
byte a, b;
int d = a + b;
你的提议会得到令人惊讶的结果.
其他提议都没有更好的理解.
我们考虑了这一点并选择不那样,理由是我们试图尽量减少不可见的截断.
另外,作为务实的程序员,除了在数据结构中节省些空间之外,我发现短几乎无用.用短代表有问题.
作为具有手动编码汇编优化经验且熟悉SIMD编译器内在函数的务实程序员,在C代码中用短作为临时代码实际上非常适合于原型设计/测试单个16位通道的行为.作为奖励,编译器中自动向量化器也可能会有所收获.但是大量的强制类型转换是有问题的.
我也不太信任自动矢量化质量,但该功能是GCC和LLVM后端免费提供的.现在,过度偏执字节/短变量错误,会迫使用户选择以下两种没有吸引力的选项:1,用丑陋的转换减小代码,2更改临时变量类型为整数,并浪费一些向量化机会.
当信噪比不好时,用户很自然就开始忽略错误消息.初学者经过有效培训,可应用强制转换,而不会思考关闭烦人的编译器,导致这样
VRP只是创可贴,帮助不大,并会带来很多不便.
我建议:
实现与Rust类似的wrapping_add,wrapping_sub,wrapping_mul内置函数,这很容易且无成本.
在其中一个D编译器(很可能是GDC或LDC)中实现实验-ftrapv选项,以在运行时捕获有符号和无符号溢出.或者,可添加函数属性来更细粒度控制该功能.是的,我知道这违反了当前的需要二进制补码解决所有的的D语言规范,但对花哨的实验选项来说并不重要.
用-ftrapv运行些测试并在Phobos中检查实际触发了多少算术溢出.如果包装行为是预期的,则用内置函数替换受影响的算术运算符.
从长远来看,请考虑更新语言规范.
好处:即使-ftrapv开销很大,也是在应用中测试算术溢出安全性的有用工具.有总比没有好.
我知道D是如何工作的.我知道为什么.见鬼,是我在dmd中实现了部分VRP代码,并向许多新用户解释了它.它实际上作用不大.
强制显式转换很少能防止真正的错误,代价是,使语言更难使用,并在未来产生了自己的问题.
放宽规则会减少大量误报的负担,强制有害转换,且保持精神规则.它不仅是不可见的截断,它还是有漏洞的不可见的截断.
良好代码会用检查漏洞的窄转换,但类型本身无趣,返回类型上重载会更好.但如果检查溢出,最好是默认.
byte x = narrow(expression);
//如果是默认,如下取消检查
byte x = uncheck(expression);
我每天都用D,我没遇见问题,我在src/dmd/*.d中:
grep -w cast *.d
未发现你提到的强制转换类别的short/ushort转换.当然,也许编码风格不同.
在phobos/std/*.d上同样查找,我写得很少,强制转换为short/ushort的实例为零.
“很少”,这类错误确实很少见但也很重要.这正是我们想要抓的东西.
通常应用向量类型,而不是依赖自动向量化.自动矢量化问题之一是,一些小改动可能会意外的阻止矢量化.
当然,允许隐式转换整数为短是*方便*.但你就没有安全的整数数学了.
正如我反复提到的,没有快速,方便且不隐藏错误的解决方案.
写个dip吧.
有趣的是,字节版代码更小.
0000000000000000 <_D4main5testbFPhQcQeZv>:
0: 8a 02 mov (%rdx),%al
2: 41 f6 20 mulb (%r8)
5: 88 01 mov %al,(%rcx)
7: c3 ret
0000000000000000 <_D4main5testiFPiQcQeZv>:
0: 8b 02 mov (%rdx),%eax
2: 41 0f af 00 imul (%r8),%eax
6: 89 01 mov %eax,(%rcx)
8: c3 ret
此外,ARM64的大小没有区别:
testb:
ldrb w0, [x0]
ldrb w1, [x1]
mul w0, w0, w1
strb w0, [x2]
ret
tests:
ldrh w0, [x0]
ldrh w1, [x1]
mul w0, w0, w1
strh w0, [x2]
ret
testi:
ldr w0, [x0]
ldr w1, [x1]
mul w0, w0, w1
str w0, [x2]
ret
我不认为成为库类型是耻辱.根据语言的不同,它们可能与内置类型一样有用且方便.本线程中提到了C++的std::byte,它就是库类型.
感谢支持,GDC已在C/C++语义上支持-ftrapv选项(捕捉有符号溢出,无符号溢出,小于int由于整体提升而在监控下的类型).现在我需要一些试验,检查如何与Phobos和其他D代码交互.修补GCC源代码来测试是否可抓无符号溢出也会很有趣.
但总之,这很有前途.它可保护一些32位和64位计算的算术溢出错误.在大型复杂软件中解决此类算术溢出问题,是我对D语言的主要关注点之一.
尽管会降低速度,DMD现在用x87圆整浮计算.
如果CPU上有SIMD浮点指令,与其他编译器一样,则用它而不是x87.
浮字面应圆整至他们的精度.
除了C和C++标准外,还有IEEE754和通用实践,特别是32/64位IEEE754.编译器至少用一组合适的标志,实现了多个标准,并明确记录每组标志的保证.
//linux中
void main(){
import std.stdio;
assert(42*6==252);
//常折叠,用扩展精度,由于双精圆整,整体结果不太准
assert(cast(int)(4.2*60)==251);
//无常折叠,用双精精度,更准确
double x=4.2;
assert(cast(int)(x*60)==252);
}
4.2和60是命名常量,结果为251或252不重要,程序可正常工作,但是,由于结果有时是251,有时是252,导致难以追踪且不一致.在编译完全相同的表达式时,我甚至在Windows与linux结果不同.虽然这是LDC,但不确定DMD也有该问题.
注意,这是最近才发生的,因而我强烈反对"增强"精度.
浙公网安备 33010602011771号