为什么没有2字节浮点数,并且是否已经有了相关实现?

51

假设我内存十分紧张,想要一个更小的范围(类似于shortint之间的区别)。着色器语言已经支持了half作为浮点类型,并且可以达到一半的精度(而不仅仅是将值转换回和介于-1和1之间的值,即像这样返回一个浮点数:shortComingIn / maxRangeOfShort)。是否已经有存在2字节浮点数的实现?

我还想知道任何(历史上的?)没有2字节浮点数的原因。


它在IEEE术语中被称为半精度浮点数,并且有实现,但不是在C标准原语中(C ++通过扩展使用)。 C标准仅规定单精度,双精度和长双精度浮点数(可能是80位或128位)。 - wkl
4
问题就应该只是问题,如果你想要关于C++中实现“half”的参考文献,那就是一个问题。如果你对“float”为什么是四字节的实体的历史原因感兴趣,那就是另一个问题。 - T.J. Crowder
1
你可以使用 half C++ 库 http://half.sourceforge.net/ - KindDragon
1
半精度浮点数已经在IEEE规范中使用了十年。有人知道为什么它还不是C++中的内置类型吗? - All The Rage
4
不需要无礼,兄弟。世界上最快的处理器具有半精度硬件支持。在机器学习、图形和视频游戏中经常使用它。电影业在渲染方面广泛使用它。但如果那些不了解使用情况的人定义语言,我想这可能回答了我的问题。 - All The Rage
显示剩余3条评论
9个回答

24

简述:16位浮点数确实存在,有各种软件和硬件实现

目前有两种常见的16位浮点数标准格式:IEEE-754二进制16和Google的bfloat16。由于它们是标准化的,任何了解规范的人都可以编写实现。以下是一些示例:

或者,如果你不想使用它们,你也可以设计一个不同的16位浮点格式并实现它。


通常不使用2字节浮点数,因为即使是float的精度也不足以进行正常操作,默认情况下应始终使用double,除非受到带宽或缓存大小的限制。在C和类似的语言中,使用浮点字面量时,如果没有后缀,则也是double。请参阅

然而存在小于32位浮点数。它们主要用于存储目的,比如在图形学中,当每像素96位(32位每通道* 3个通道)太浪费时,将转换为普通的32位浮点数进行计算(除了一些特殊的硬件)。OpenGL中存在各种10、11、14位浮点类型。许多HDR格式使用每个通道的16位浮点数,Direct3D 9.0以及一些GPU(如Radeon R300和R420)具有24位浮点格式。某些8位微控制器的编译器(如PIC)支持24位浮点数,因为32位浮点数的支持成本太高。8位或更窄的浮点类型不太有用,但由于其简单性,它们经常在计算机科学课程中教授。此外,小浮点数也用于ARM指令编码中的小浮点立即数。

IEEE 754-2008修订版正式添加了一种16位浮点数格式,即二进制16半精度,具有5位指数和11位尾数。

一些编译器支持IEEE-754二进制16,但主要用于转换或矢量化操作,而不是计算(因为它们不够精确)。例如,ARM的工具链具有__fp16,可以在2个变体之间选择:IEEE和替代,具体取决于您是否需要更大的范围或NaN / inf表示。GCCClang也支持__fp16以及标准名称_Float16。请参见如何在x86_64上启用__fp16类型的gcc

最近由于人工智能的兴起,另一种称为bfloat16brain floating-point format)的格式变得普遍,它是IEEE-754 binary32顶部16位的简单截断
减少尾数的动机来自Google的实验,显示减少尾数是可以的,只要仍然可以表示靠近零的微小值作为训练期间小差异之和的一部分。较小的尾数带来许多其他优点,例如减少乘法器功率和物理硅面积。
许多编译器,如GCCICC,现在也具有支持bfloat16的功能。
有关bfloat16的更多信息:

在bfloat16不足的情况下,还出现了一种新的19位类型,称为TensorFloat


MSVC通过DirectX支持“HALF”:https://learn.microsoft.com/en-us/windows/win32/dxmath/half-data-type - Matt Eding
在64位机器上,float除了类似于SIMD的向量操作外并没有太多用处。double的额外范围很有用,但即使32位的float在大多数情况下也提供了比实际需要更高的精度。你最后一次做到7个有效数字的实际事情是什么时候?从物理角度来看,这相当于将长度为500英尺的东西测量到+-1/1000英寸。有些数学运算可能会损坏这7个数字,但使用double只会部分地掩盖症状,这些数学怪癖也会损害double。真正的解决方案是使用避免这些陷阱的算法。 - Max Power
@MaxPower 如果一切都像那样简单,那么半精度浮点数和其他小浮点数就不会存在,定点数学也会非常普遍。在某些情况下,您可能不关心精度,因为动态范围更重要。但当然,您必须在数学上证明这一点,这并不是初学者可以做到的。这就是为什么它只在图像和人工智能中常用的原因。 - phuclv
@phuclv,我不确定你是否完整地阅读了我的帖子,它与你的回复关系不大。无论如何,固定点非常常见,它被称为整数运算。(在显示时加上一个小数点。) - Max Power
1
就像我所说的,除了矢量单元吞吐量之外,在64位CPU上使用double类型并不会有什么劣势。而且你需要首先考虑所使用的算法,如果不这样做,你只是盲目地摸索任何类型。正确评估有效数字在10年级化学中已经涵盖。float类型的7个数字是为了从十进制到浮点数再到十进制的保守转换,这只需要进行一次,用于输入和最终输出。(大多数数字保留8或9)在内部,float类型的精度略高于此,7是剩余的典型舍入误差。 - Max Power
显示剩余6条评论

19

关于实现:据说有人为C语言写了half,当然可以在C++中使用:https://storage.googleapis.com/google-code-archive-downloads/v2/code.google.com/cellperformance-snippets/half.c

关于为什么float是四个字节:可能是因为在此之下,它们的精度非常有限。在IEEE-754标准中,“half”只有11位有效数字精度,产生约3.311个十进制数字的精度(而在single中有24位,产生6到9个十进制数字的精度,在double中有53位,产生15到17个十进制数字的精度)。


4
没问题。10位二进制数等于3.01个十进制数字,对于大多数数字计算任务来说是不够的。 - dan04
@dan04 这是11,包括隐含的一位。 - S.S. Anne
3
好的,3.31个十进制位。虽然这并没有太大的区别。 - dan04
2
@dan04 这是一个可以表示10位的差异。 - Soleil

16

如果你的内存不足,你是否考虑放弃浮点数的概念?浮点数会使用大量比特来保存小数点的位置。如果你知道需要小数点的位置,例如你想要保存一个美元值,你可以将它保存为分:

uint16_t cash = 50000;
std::cout << "Cash: $" << (cash / 100) << "." << ((cash % 100) < 10 ? "0" : "") << (cash % 100) << std::endl;

当然,这只是一个选项,前提是您能够预先确定小数点的位置。但是如果可以,始终选择它,因为这也加快了所有计算的速度!


1
那不正确,如果现金=402,你将打印42。 - Et7f3XIV
2
@Et7f3XIV 你说得对,我当时在这个页面上的回答是多么的粗心啊,真是惊人。:( - Kira M. Backes
2
如果您包含<iomanip>头文件,您可以这样编写代码: std::cout << "现金:$" << (cash / 100) << "." << std::setfill('0') << std::setw(2) << (cash % 100) << std::endl; - Et7f3XIV
3
当你知道小数点的位置时,这被称为“定点算术”,具体信息请参考fixed-point arithmetic - phuclv
1
固定点本质上是整数运算,只是加了一个表面的小数点。float16的范围比int16大,但存在权衡。IEEE float16在整个范围内可靠地具有约3个有效十进制数字,从非常小到非常大,而int16是65536个单位的精确计数索引,无论您在哪里固定点。int16低端的精度为一位数字,但已知其完全准确,高端为5位数字。如果需要整体百分比的准确性和广泛范围,请使用float;对于像跟踪库存这样的精确计数,请使用int或固定点。 - Max Power

6

2
是的,他在问题中提到了“一半”。 - T.J. Crowder

3

如果要比Kiralein更进一步地切换到整数,我们可以定义一个范围,并允许short类型的整数值表示范围内相等的分段,如果跨越零点,则需要一些对称性:

short mappedval = (short)(val/range);

这些整数版本和使用半精度浮点数的区别如下:
  1. 整数在范围内等间隔分布,而浮点数在零附近更密集地分布。
  2. 使用整数会在CPU中使用整数运算,而不是浮点运算。这通常更快,因为整数运算更简单。话虽如此,将值映射到非对称范围需要额外的加法等操作才能检索到最终值。
  3. 绝对精度损失更可预测;您知道每个值的误差,因此可以事先计算出总损失,给定范围。相反,使用浮点数更可预测相对误差。
  4. 可能有一小部分操作可以使用两个值来执行,特别是位操作,通过将两个short装入int中实现。这可以减少所需的循环次数(如果短操作涉及int强制转换,则节约的时间可能更多),并保持32位宽度。这只是比特切片的淡化版本,其中32位并行操作,用于密码学。

2
如果你的CPU支持F16C,那么你可以很快地启动一些操作,例如:

如果你的CPU支持F16C,那么你可以通过以下方式快速启动:

注意:本文中"Original Answer"翻译成 "最初的回答"

// needs to be compiled with -mf16c enabled
#include <immintrin.h>
#include <cstdint>

struct float16
{
private:
  uint16_t _value;
public:

  inline float16() : _value(0) {}
  inline float16(const float16&) = default;
  inline float16(float16&&) = default;
  inline float16(const float f) : _value(_cvtss_sh(f, _MM_FROUND_CUR_DIRECTION)) {}

  inline float16& operator = (const float16&) = default;
  inline float16& operator = (float16&&) = default;
  inline float16& operator = (const float f) { _value = _cvtss_sh(f, _MM_FROUND_CUR_DIRECTION); return *this; }

  inline operator float () const 
    { return _cvtsh_ss(_value); }

  inline friend std::istream& operator >> (std::istream& input, float16& h) 
  { 
    float f = 0;
    input >> f;
    h._value = _cvtss_sh(f, _MM_FROUND_CUR_DIRECTION);
    return input;
  }
};

数学计算仍使用32位浮点数(F16C扩展仅提供16/32位浮点数之间的转换 - 没有指令可用于使用16位浮点数进行算术运算)。

最初的回答


这可以在不使用 immintrin.h 的情况下完成。请参考此答案:https://dev59.com/P7Lma4cB1Zd3GeqPeKht#64493446 - wolfram77
1
@wolfram77 我很确定你提供的链接是关于 bfloat16 的,而这个回答则是关于半精度浮点数。 - Tara

1
可能在不同的实现中有各种类型。一个类似于stdint.h的浮点数等价物似乎是个好主意。按它们的大小来调用(或者别名?)这些类型(float16_t?)。现在一个float是4个字节,但它可能不会变得更小。像half和long这样的术语随着时间的推移大多变得毫无意义。对于128位或256位的计算机,它们可以意味着任何东西。
我正在处理图像(1+1+1字节/像素),我想表达每个像素值相对于平均值的关系。所以要使用浮点数或精心固定的点,但请不要比原始数据大4倍。一个16位的浮点数听起来很合适。
这个GCC 7.3不知道"half",也许在C++上下文中知道。

128位和256位处理是一个专业领域,在一般计算机市场上不太可能看到,除了在64位CPU中可能有一个单独的长数字单元的情况下。无论如何,“long double”和“long long int”已经在C++中保留(大概是为了128位),尽管大多数编译器目前将它们设置为重复的64位类型或x86_64机器上的x87 80位浮点型。long double不应与“double double math”混淆,后者是两个64位浮点数拼接在一起的(比使用软件实现的任意精度数学稍快)。 - Max Power
自真空管时代以来,主机CPU的位数一直在32位和64位之间。8位和16位仅用于低成本或低功耗。很少有用例需要超过7个有效数字的精度(32位)。64位浮点数约为15个有效数字(x87单元接受64位输入,内部使用80位并返回19个有效数字的64位)。128-256位计算非常小众。 由于操作原因,64位地址空间不太可能在单台机器上被超越,而基本物理限制则是128位。8 *(2 ^ 128)硅原子[128位地址空间中的位数]重达130吨。 - Max Power
@MaxPower 你确定吗?第一台64位计算机于1961年发布,比真空管时代晚得多。而且,“long long int”已经在C++中保留了[可能是为了128位],这是绝对错误的。自C++11以来,long long已经存在,并且至少有64位。 - phuclv
1
@phuclv 在发布回复之前,你需要努力理解你所回复的内容。是的,128位至少是64位,问任何人数学都能解决问题。 if(128>=64)std::cout<<"True\n"; else std::cout<<"False\n"; ENIAC在硬件上是十进制的,可以计算10或20位十进制数字。(这比40位和80位二进制好一点);EDVAC使用44位字长;SWAC使用37位字长,具有单精度或双精度(74位);EDSAC使用34位,使用两个17位字;曼彻斯特Mark 1使用40位数字和20位指令;MEG / Mercury浮点单位使用40位,30位尾数和10位指数。 - Max Power

1

在clang C编译器中,有2字节浮点数可用,该数据类型表示为__fp16


1
各种编译器现在支持三种不同的半精度格式:
  • __fp16主要用作存储格式。一旦在其上进行计算,它就会被提升为float。__fp16上的计算将得到一个float结果。__fp16具有5位指数和10位尾数。
  • _Float16与__fp16相同,但用作交换和算术格式。_Float16上的计算将给出_Float16结果。
  • __bf16是一种精度较低的存储格式。它具有8位指数和7位尾数。

所有三种类型都受到ARM架构编译器的支持,现在也受到x86处理器编译器的支持。AVX512_FP16指令集扩展将由英特尔即将推出的Golden Cove处理器支持,并且最新的Clang、Gnu和Intel编译器也支持它。在支持AVX512_FP16的编译器上,_Float16的向量被定义为__m128h、__m256h和__m512h。

参考资料:

https://developer.arm.com/documentation/100067/0612/Other-Compiler-specific-Features/Half-precision-floating-point-data-types

https://clang.llvm.org/docs/LanguageExtensions.html#half-precision-floating-point


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