d算术混淆大论战

原文
引文


隐式转换易错


问题是如何说服沃尔特


我想要输出隐式转换的警告的编译时选项


我更希望,移植CD时,可报错,而不是保留地雷代码.应该是这样的-vimplicit-conversions隐式开关.


C系外系统编程语言倾向于用显式转换.


第1个示例,在D中修复了.乘法示例有意思,其他在速度相关时,意义不大.
奇怪的是,没有更多signed -> unsigned间的转换,那才是个大坑.


包装和非包装(溢出陷阱)算术运算,现代编程语言倾向于有单独的运算符或内置操作符.
抓此类错误与检查数组边界一样有用.处理器早晚要加缺失指令来加快速度.


C++已用-Wconversion修复了.
当然,如果用模板库,可能会收到不想要警告.因此,好语法消除内联警告,鼓励打开最大严格性,并在方便时可选的禁用它.内联静默警告是tsjs好的地方.
模板时要注意,可基于有无符号限制模板参数.
有符号模运算有点麻烦,但我自己重载了.

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().


我不信.DPhobos的设计可大量防止与内存安全无关的错误,有时甚至以效率代价.例如,@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差.使用byteshort类型时,这是不断烦恼根源.
值区间传播仅适用于单个表达式,过于保守,无法在实际代码中提供太多帮助.


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);
}

当前规则要求,在构造函数调用中显式转换.然后,稍后,重构Thingint.它仍会编译,仍然存在显式转换,现在砍掉位.
显式转换问题,一旦写入它们,很难撤销.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一样,是的,乘法产生一个高字,但它可能不适合,我不希望编译器烦我.
折衷方案会平衡合法的安全问题与意外丢失或重构更改(如重构为整数,现在输入类型增长,并且编译器可再次发出错误)与几乎无处不在烦人转换.
移除大部分转换,使剩下转换更加突出,它们是潜在的问题.


dC整提升转换的成功一样,零惊奇.
但也有区别.
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应用于大多数计算,shortbyte用于存储.
(现代CPU长期以来一直刻意优化和调整C语义.)
现代机器*肯定*了解C的工作原理.


在只期望正值的API中非常有用,标记为uint表明预期,处理位掩码时,也很有用.对系统编程语言,也很重要.更反映现实.
需要库类型来操作位掩码会使D成为系统编程语言的一个完全笑话.
因而,尽量用.


用户并不关心编译器如何执行指令.他们关心结果,如果赋值为字节,他们可能不在乎失去额外精度,不然,为何不用?


不再,小整上可能较快.
1.你非常小心地演示了短算术,而不是与x86上的算术大小相同的字节算术.
2.循环计数(或字节计数)不是明智的语言设计方法.可能与语言实现有关;整个程序性能可能与语言设计有关;但变化是微不足道的,不应妨碍正确的语义.
3.你代码示例完全按你建议一样,用短算术存储.此时,用算术而不是算术,会产生相同结果及更小代码.
4.(上接3)在更大,更有趣表达式中,不管语言语义,编译器一般都会自由地使用作为临时变量.


较大代码大小肯定会给指令缓存带来更大压力,但减速不大.利用SIMD指令的自动向量代码看起来有点不同.
处理大型数组时,性能确实提高了很多.并且16位版本大约比32位版本快两倍(因为每个128XMM寄存器代表8个短4个整数).
如果希望D语言对SIMD友好,那么可以鼓励局部变量使用shortbyte.


int a = int.max;
long b = a + 1;
writeln(b > 0); // 假

是的,我希望编译器确定是否需要溢出,并基于此生成适当指令.
对于int->long不经常出现原因是:因为a)不常转换intlong,且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^3232位寄存器上隐式用的.2^8是隐式截断.
前者,有两个显式截断,可重写为后者,去掉中间截断,得到与提升完全相同结果.


访问这些字节寄存器需要额外的REX字节.
他们需要为子表达式插入强制转换到整中.这不好.
考虑比加载和存储更复杂的表达式.
考虑:

byte a, b;
int d = a + b;

你的提议会得到令人惊讶的结果.
其他提议都没有更好的理解.


我们考虑了这一点并选择不那样,理由是我们试图尽量减少不可见的截断.
另外,作为务实的程序员,除了在数据结构中节省些空间之外,我发现几乎无用.用代表有问题.


作为具有手动编码汇编优化经验且熟悉SIMD编译器内在函数的务实程序员,在C代码中用作为临时代码实际上非常适合于原型设计/测试单个16位通道的行为.作为奖励,编译器中自动向量化器也可能会有所收获.但是大量的强制类型转换是有问题的.


我也不太信任自动矢量化质量,但该功能是GCCLLVM后端免费提供的.现在,过度偏执字节/短变量错误,会迫使用户选择以下两种没有吸引力的选项:1,用丑陋的转换减小代码,2更改临时变量类型为整数,并浪费一些向量化机会.
信噪比不好时,用户很自然就开始忽略错误消息.初学者经过有效培训,可应用强制转换,而不会思考关闭烦人的编译器,导致这样
VRP只是创可贴,帮助不大,并会带来很多不便.
我建议:
实现与Rust类似的wrapping_add,wrapping_sub,wrapping_mul内置函数,这很容易且无成本.
在其中一个D编译器(很可能是GDCLDC)中实现实验-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/64IEEE754.编译器至少用一组合适的标志,实现了多个标准,并明确记录每组标志的保证.

//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.260命名常量,结果为251252不重要,程序可正常工作,但是,由于结果有时是251,有时是252,导致难以追踪且不一致.在编译完全相同的表达式时,我甚至在Windowslinux结果不同.虽然这是LDC,但不确定DMD也有该问题.
注意,这是最近才发生的,因而我强烈反对"增强"精度.

posted @   zjh6  阅读(19)  评论(0编辑  收藏  举报  
相关博文:
阅读排行:
· 分享一个免费、快速、无限量使用的满血 DeepSeek R1 模型,支持深度思考和联网搜索!
· 基于 Docker 搭建 FRP 内网穿透开源项目(很简单哒)
· ollama系列01:轻松3步本地部署deepseek,普通电脑可用
· 25岁的心里话
· 按钮权限的设计及实现
点击右上角即可分享
微信分享提示