浮点数-背景知识
当需要在代码中表示非整数值时,通常使用浮点数表示。在C语言中,通常使用标准的float或double原始类型来实现。关于浮点数的完整细节(特别是IEEE 754格式)超出了本教程的范围。本节将着重介绍相关的一般概念。
浮点数表示(无论是标准类型如float还是非标准类型如float_s32_t)通过一种科学记数法来近似实数值。每个可表示的值都使用一对整数和进行编码,其中
这里,(有符号)整数是尾数(mantissa),(有符号)整数是指数(exponent)。因此,值是某个整数乘以2的某个幂次方。
浮点数表示是固定大小的编码,每个可表示的值都使用相同数量的位存储在内存中。在xcore.ai上,单精度(float)的值是32位对象,双精度(double)的值是64位对象。通常,尾数和指数本身也被分配了固定数量的位。32位IEEE 754浮点数使用8位表示指数,24位表示尾数,包括一个符号位。double类型有53位尾数和11位指数。
与之相反,lib_xcore_math中的非标准浮点标量类型float_s32_t使用总共64位,其中32位用于尾数,32位用于指数。
实际上几乎不需要超过8位的指数。然而,使用整个字作为指数具有体系结构上的优势。在使用lib_xcore_math中的非标准浮点类型时,通常意味着在不同的块浮点实体之间进行桥接,因为float_s32_t类型本质上等效于只有一个元素的32位BFP向量。
在32位BFP API中,如果返回float_s32_t值,则16位API中相应的函数通常返回标准的float值。原因是float类型有24位尾数。从32位API返回它会立即损失高达8位的精度,而在16位API中不会出现这种损失。
此外,虽然使用double值比float_s32_t更容易,不会导致精度损失,但是xcore.ai不支持double硬件。在软件中实现的double算术成本过高(如Part 1A中所示),并且与float_s32_t具有相同的内存占用。
由于其大小,float_s32_t(和相关类型)的数组会浪费内存,不建议使用。如果发现自己需要这样做,请考虑使用块浮点API或float向量API。
一个方便的思考方式(与将在Part 3中介绍的块浮点概念完美契合)是考虑在具有_固定_指数的浮点表示中可表示的值的范围和间隔。
对于以指数表示的float_s32_t的值,32位尾数可以取int32_t值的标准范围,从INT32_MIN()到INT32_MAX()。由于,可以表示任意普通的32位整数。还要注意,可表示值的间隔(对于)为。
可表示值的上界、下界和间隔是的指数级增长,因此是的线性增长。因此,将增加会使这三个属性都加倍。同样,将减小会使它们减半。
这对于块浮点算术来说是一个特别重要的思维模型,因为(如Part 3中所示),对于BFP操作,输出的指数通常是在计算任何输出尾数之前选择的(通常只有关于输入尾数的元信息,以_头空间_的形式)。
这种框架还弥合了四种离散算术的差距,即整数、定点、浮点和块浮点。这四种算术可以看作是更一般算术的特殊化,其中操作值的具体细节是表示特定的,但整体数学逻辑是统一的。
统一逻辑
假设我们有一个包含个实数值的_向量_,其中元素为,。我们可以以一种广义的方式描述特定值的抽象表示:
这里,有一个尾数(某个位深度)向量,以及一个长度为的指数向量。在这里,每个尾数对应于一个指数。
为了简化,我们将包含附加约束。如果,则所有尾数都使用相同的指数,如果,则每个尾数都有自己的指数。
这个附加约束不是_必需的_。有时候,在不同的尾数范围使用不同的指数是有用的。当的元素涵盖较大的动态范围时,这种情况特别常见。
例如,音频信号通常在较低频率上具有绝大部分功率,这在它们的频谱中表现出来,其中靠近直流(DC)的频率分量的谱幅比靠近奈奎斯特率的频率分量大几个数量级。
在这种情况下,使用2个或更多与不同频率范围对应的指数是有用的。这有助于保持较高频率分量的算术精度。
如果我们将“浮点数”理解为“指数_不一定_是_固定的_”,而 不是指“指数_一定_是_动态确定的_”(这是我们可能迄今为止隐含地假设的),我们可以说:
- 所有算术都可以视为向量算术,无论为何值
- 标量算术是的向量算术
- 整数算术是的算术
- 定点算术是具有常数的算术
- 块浮点算术具有
- 普通(非块)浮点算术对应于
这些不同类型的算术各有优缺点。
在xcore.ai上,单精度float操作由硬件浮点单元(FPU)加速,包括一个时钟周期的融合乘累加(FMA)指令。
PCM-浮点数转换
在Part 1的每个阶段中,从tile[0]接收的PCM样本会转换为浮点数,并且浮点数输出样本在发送到tile[0]之前会转换为PCM样本。这两个步骤分别在frame_rx()和frame_tx()函数中进行。
在每个循环阶段中,filter_task()函数 将读取一帧新的输入音频样本(使用rx_frame()),计算一帧的输出音频样本(使用filter_sample()),然后将输出样本帧发送回wav_io线程(使用tx_frame())。
在tx_frame()函数中,将浮点数值转换为32位PCM值的逻辑与rx_frame()中的逻辑相反。考虑将double值0.123456转换为具有31位小数部分的32位定点值的情况。
现在考虑将浮点数值进行相同的转换。
最后,考虑将浮点数值进行相同的转换。
尽管被转换为,即int32_t类型的最小值,但被转换为,这是无法用有符号32位整数表示的值。
因此,使用输出指数为,可以将浮点数值转换为32位整数而不会溢出的范围为。
组件函数
在第一部分中,每个阶段的行为由4个组件函数定义:
rx_frame()tx_frame()filter_sample()filter_task()
这些函数在接下来的大多数阶段中也会被定义。以这种方式组织阶段可以更容易地进行不同实现之间的比较。
在查看每个阶段的代码时,这些函数是我们要检查的函数。
这些函数的_签名_在所有阶段中并不相同。
filter_task()
这是过滤线程的线程入口点。该函数通常会声明输入和输出样本数据的所需缓冲区,对它们进行初始化,然后进入一个无限循环。
在每次循环迭代中,filter_task()会使用rx_frame()读取一帧新的输入音频样本,使用filter_sample()计算一帧输出音频样本,并将输出样本帧发送回tile 0上的wav_io线程(使用tx_frame())。
filter_sample()
每次调用filter_sample()都会使用接收样本的历史记录计算一个输出样本。这个函数的实现会在本教程的每个阶段中改变。
rx_frame()
这个函数从在tile 0上运行的wav_io线程通过通道接收一帧输入音频样本。在大多数情况下,该函数会将接收到的样本以逆时间顺序存储到样本历史中,以确保样本数据的顺序与滤波器系数的顺序相匹配。
我们恰好使用的是对称滤波器,所以在我们的情况下顺序实际上并不重要。然而,一般情况下顺序确实很重要。
tx_frame()
这个函数使用通道将一帧输出音频样本发送回tile 0上的wav_io线程。