Skip to main content
欢迎来到PAWPAW技术文档网站了解更多信息

浮点数-背景知识

当需要在代码中表示非整数值时,通常使用浮点数表示。在C语言中,通常使用标准的floatdouble原始类型来实现。关于浮点数的完整细节(特别是IEEE 754格式)超出了本教程的范围。本节将着重介绍相关的一般概念。

浮点数表示(无论是标准类型如float还是非标准类型如float_s32_t)通过一种科学记数法来近似实数值。每个可表示的值xx都使用一对整数mmpp进行编码,其中

x=m2px = m \cdot 2^{p}

这里,(有符号)整数mm是尾数(mantissa),(有符号)整数pp是指数(exponent)。因此,值xx是某个整数乘以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中介绍的块浮点概念完美契合)是考虑在具有_固定_指数pp的浮点表示中可表示的值的范围和间隔。

对于以指数p=0p=0表示的float_s32_t的值xx,32位尾数mm可以取int32_t值的标准范围,从INT32_MIN231-2^{31})到INT32_MAX23112^{31}-1)。由于20=12^0=1xx可以表示任意普通的32位整数。还要注意,可表示值的间隔(对于p=0p=0)为1=2p1=2^p

可表示值的上界、下界和间隔是pp的指数级增长,因此是2p2^p的线性增长。因此,将pp增加11会使这三个属性都加倍。同样,将pp减小11会使它们减半。

这对于块浮点算术来说是一个特别重要的思维模型,因为(如Part 3中所示),对于BFP操作,输出的指数通常是在计算任何输出尾数之前选择的(通常只有关于输入尾数的元信息,以_头空间_的形式)。

这种框架还弥合了四种离散算术的差距,即整数、定点、浮点和块浮点。这四种算术可以看作是更一般算术的特殊化,其中操作值的具体细节是表示特定的,但整体数学逻辑是统一的。

统一逻辑

假设我们有一个包含NN个实数值xˉ\bar{x}的_向量_,其中元素为xkx_kk{0,1,...,(N1)}k \in \{0,1,...,(N-1)\}。我们可以以一种广义的方式描述特定值xkx_k的抽象表示:

xkmk2pkLNfor k{0,1,...,(N1)}x_k \approx m_k \cdot 2^{p_{\lfloor \frac{kL}{N} \rfloor }} \quad \text{for }k \in \{0,1,...,(N - 1)\}

这里,有一个尾数(某个位深度)向量mˉ\bar{m},以及一个长度为LL的指数向量pˉ\bar{p}。在这里,每个尾数mkm_k对应于一个指数pkLNp_{\lfloor\frac{kL}{N}\rfloor}

为了简化,我们将包含附加约束L{1,N}L \in \{1,N\}。如果L=1L=1,则所有尾数都使用相同的指数,如果L=NL=N,则每个尾数都有自己的指数。

信息

这个附加约束不是_必需的_。有时候,在不同的尾数范围使用不同的指数是有用的。当xˉ\bar{x}的元素涵盖较大的动态范围时,这种情况特别常见。

例如,音频信号通常在较低频率上具有绝大部分功率,这在它们的频谱中表现出来,其中靠近直流(DC)的频率分量的谱幅比靠近奈奎斯特率的频率分量大几个数量级。

在这种情况下,使用2个或更多与不同频率范围对应的指数是有用的。这有助于保持较高频率分量的算术精度。

如果我们将“浮点数”理解为“指数_不一定_是_固定的_”,而不是指“指数_一定_是_动态确定的_”(这是我们可能迄今为止隐含地假设的),我们可以说:

  • 所有算术都可以视为向量算术,无论NN为何值
  • 标量算术是N=L=1N = L = 1的向量算术
  • 整数算术是pˉ=0\bar{p} = 0的算术
  • 定点算术是具有常数pˉ\bar{p}的算术
  • 块浮点算术具有L=1L = 1
  • 普通(非块)浮点算术对应于L=NL = N

这些不同类型的算术各有优缺点。

在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()中的逻辑相反。考虑将double0.123456转换为具有31位小数部分的32位定点值的情况。

output_exp=31samp_f=0.123456sample_outround(ldexp(0.123456,output_exp))=round(ldexp(0.123456,31))=round(0.123456231)=round(265119741.247488)=265119741\begin{aligned} \mathtt{output\_exp} &= -31 \\ \mathtt{samp\_f} &= 0.123456 \\ \\ \mathtt{sample\_out} &\gets \mathtt{round} (\mathtt{ldexp}(0.123456, -\mathtt{output\_exp})) \\ &= \mathtt{round}(\mathtt{ldexp}(0.123456, 31)) \\ &= \mathtt{round}(0.123456 \cdot 2^{31}) \\ &= \mathtt{round}(265119741.247488) \\ &= 265119741 \\ \end{aligned}

现在考虑将浮点数值1.0-1.0进行相同的转换。

output_exp=31samp_f=1.0sample_outround(ldexp(1.0,output_exp))=round(1.0231)=2147483648=INT32_MIN\begin{aligned} \mathtt{output\_exp} &= -31 \\ \mathtt{samp\_f} &= -1.0 \\ \\ \mathtt{sample\_out} &\gets \mathtt{round} (\mathtt{ldexp}(-1.0, -\mathtt{output\_exp})) \\ &= \mathtt{round}(-1.0 \cdot 2^{31}) \\ &= -2147483648 \\ &= \mathtt{INT32\_MIN} \\ \end{aligned}

最后,考虑将浮点数值+1.0+1.0进行相同的转换。

output_exp=31samp_f=1.0sample_outround(ldexp(1.0,output_exp))=round(1.0231)=2147483648=INT32_MAX+1\begin{aligned} \mathtt{output\_exp} &= -31 \\ \mathtt{samp\_f} &= 1.0 \\ \\ \mathtt{sample\_out} &\gets \mathtt{round} (\mathtt{ldexp}( 1.0, -\mathtt{output\_exp})) \\ &= \mathtt{round}( 1.0 \cdot 2^{31}) \\ &= 2147483648 \\ &= \mathtt{INT32\_MAX} + 1 \\ \end{aligned}

尽管1.0-1.0被转换为INT32_MIN\mathtt{INT32\_MIN},即int32_t类型的最小值,但+1.0+1.0被转换为(INT32_MAX+1)=INT32_MIN(\mathtt{INT32\_MAX}+1) = -\mathtt{INT32\_MIN},这是无法用有符号32位整数表示的值。

因此,使用输出指数为31-31,可以将浮点数值转换为32位整数而不会溢出的范围为[1.0,1.0)[-1.0, 1.0)

组件函数

第一部分中,每个阶段的行为由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线程。