蓝毛毛虫:嗯,你不会让我们失望的。 我们知道,我们自己坐着:他们正在等待我们的转型。 什么啊 但是什么都没有! 我们坐着,抽烟,等待...
爱丽丝娃娃:什么?
蓝毛毛虫:什么,为什么! 转型。 房子变成烟雾,烟雾变成女士,女士变成母亲。 你去。 不要干涉,不要跳跃,否则你自己会过早地变成某种蝴蝶。
通过在一个专门用于Arduino的论坛上浏览代码,我发现了一种有趣的使用浮点数(PT)的方法。 这种格式的数字的第二个通用名称是浮点数,但是在这种情况下出现的缩写(PP)亲自引起我的完全不同的关联,因此我们将使用此选项。 第一个印象(从可见的代码中得出)是在这里写的是哪种垃圾(我必须说,第二个是一样的,尽管有细微差别,但稍后会更多),但问题出现了-真的有必要吗-给出了答案进一步的文字。
第一部分-提问
我们解决了这个问题-我们需要在控制台上打印一个浮点数(转换为符号表示形式),而无需使用用于此目的的打印选项。 为什么我们要自己做-
- 使用%f格式需要连接库以使用浮点和prntf函数的扩展版本(或者,更不可能使用其截断的版本),这导致可执行模块的大小显着增加,
- 标准解决方案需要花费大量时间(始终使用双精度数字),这在这种特殊情况下可能是不可接受的,
- 好吧(最后但并非最不重要),这很有趣。
首先,请考虑以上材料中提出的选项,例如:
for (float Power10=10000.0; Power10>0.1; Power10/=10.0; ) {char c=(int)(Fdata/Power10); Fdata -=Power10*c; };
我们同意他完全解决了这个问题。 此外,这不是一个坏选择,因为它的速度可以接受。 现在让我们仔细看看-我们看到了PT数的除法,但是如果我们深入研究问题的实质,事实证明它几乎与相应位深度的整数除法一样快。 实际上,在评估算法性能之前,您应该评估各种基本操作的性能,我们将这样做。
第二部分-基本运营绩效评估
第一个有趣的操作是数字整数的加法(从时间的意义上讲,它们是等效的),我们可以假设它花了一个时间单位(时钟周期),并附带以下警告:仅对“本机”数据如此。 例如,对于MK系列AVR,它是8位字,对于MSP430,它是16位字(当然,尺寸更小),对于Cortex-M,它是32位字,依此类推。 然后,将数字加长比本地数字长H倍的数字的操作可以估计为H个周期。 有一些例外,例如AVR控制器中的AddW,但它不会取消规则。
接下来的操作是整数的乘法(但不是除法,它在速度方面有所不同),对他而言,并非一切都那么简单。 首先,乘法可以在硬件中实现,例如,在AVR MEGA中,它需要2个时钟周期,而在改进的51中则需要6个时钟周期(用于乘以本机数字)。
但是考虑没有硬件实现的情况,我们必须以子例程的形式实现乘法。 由于将H位数字相乘时得到2H位乘积,因此可以发现经典版本的移位估计如下:我们需要对因子进行H移位,每个移位1个时钟周期,对第二因子进行H移位,长度为2 H,每个移位2个时钟周期,然后H做出决定并,平均来说,N / 2个数字的相加长度为2H,总之,一个2个度量的周期的组织。 总+2++2/ 2 +2=7滴答声,从它们实际执行算术运算仅需N滴答声(哇效率,尽管我们设法绕过了引擎)。
也就是说,要将两个8p数乘以8p MK,需要56个周期,而要将16p数相乘,已经有112个周期(略少一些,但我们忽略了确切的值),这比我们想要的要多。 幸运的是,移位的方向可以修改,并且有一种独特的乘法方式,该方法仅需要对2H位数字进行H次移位,并对原始数进行H / 2加法运算,从而将乘法算法的运算时间缩短为0 + 2 + 1 + 1/2 + 2 =5.5-当然,它不能与硬件实现进行比较,但至少可以有所收获而不会损失功能。 该算法有改进,例如每个周期分析2位,但是它们并没有彻底改变这种情况-乘以数量级的时间超过了相加的时间。
但是,使用除法运算时,情况会更糟-即使是由硬件实现的除法运算,乘法运算所损失的损失几乎是原来的两倍,并且有些具有硬件乘法运算的MK却没有进行硬件除法运算。 在某些条件下,除法可以用乘以乘以倒数来代替,但是这些条件是特定的,并且给出相似的结果-要求相乘两次,然后再相加,因此损失了2倍。 如果我们将除法实现为一个子程序,则需要除数2H长N个移位,除数2H长H的减法,结果的H移位,周期的2H组织,但所有这些操作之前都要进行对齐,这将需要另外5H周期,所以总数为2 + 2 +1 + 2 + 5 =12,大约是乘法的2倍。
好了,现在让我们看一下PT运算,这里的情况有点自相矛盾-乘法运算所需的时间几乎与整数(相当于位容量,通常为24位)所需的时间一样,因为我们必须将尾数相乘并只求和,所以规范化不会必填项。 用除法也很好,除以尾数并减去阶数,再也不需要归一化。 因此,对于这两个操作,尽管与整数相比损失不大,但它确实占有一席之地。
但是加法和减法运算首先需要对齐顺序(这些是移位,尽管有细微差别,但可以有很多),然后是运算本身,以及(减去时)归一化(当相加时,也不需要超过1个移位) ),这很浪费时间,因此PT的此类运算比整数的运算要慢得多,尤其是相对而言。
让我们回过头来,同意,根据先前的估计,建议的方法可能不会太长,特别是因为它可以立即给出结果,但是它有很大的局限性-它适用于输入PT值的非常有限的范围。 因此,它将寻求一种通用(或多或少)的解决方案。
立即提出一个保留意见,即我们的解决方案通常不应使用浮点运算(从字面上讲)来强调我们选择的优点。 对于一个困惑的问题,即如果操作不可用,那么这种类型的数字将如何出现,我们回答-例如当从光传感器读取信息时(如原始示例中的信息),它很可能会出现,该信息会产生PT格式的数据。
您可以在许多站点上轻松找到PT数量的确切排列方式,最近有一篇关于哈布雷的文章,这应该没有任何问题。 但是,PT格式“如果我是导演”会引起很多问题,为什么会这样,而不是其他。 我将给出其中一些答案,如果有人知道更正确,请发表评论。
第一个问题是为什么尾数存储在直接代码中而不是其他代码中? 我的答案是因为使用带隐藏(可选)位的标准化尾数更容易。
第二个问题是为什么订单以偏移量存储,而不是其他方式? 我的回答是,在这种情况下,很容易将两个PT的模块比较为整数,而使用其他方法则比较复杂。
第三个问题是为什么负号用统一而不是零编码,因为这样可以将两个点简单地比较为整数? 我的答案是我不知道,只是“在这里被接受”。
第三部分-必要说明
在上一段中,我可以给出难以理解的术语,所以关于数字的表示要少一些。 当然,它们是不同的,否则将无需讨论它们。 马上,我们注意到在MK的内存中(计算机也是如此,尽管我对最现代的体系结构不是那么严格-它们是如此复杂以至于可以期望得到一切)没有数字,只有基本存储单元-位分组个字节,然后进一步转换成单词 当我们谈论数字的表示时,这意味着我们以一种或另一种方式解释了一组特定长度的位,也就是说,我们设定了一条定律,通过该定律,我们可以找到与给定位集合相对应的某个数字,仅此而已。
可以发明无数种这样的定律,但是其中一些定律在进行各种操作方面将具有许多有用的特性,因此它们将在实践中更经常地应用。 例如,这些特性中的一种是隐含的,即是确定性,另一种是与环境的独立性-尽管有些细微差别,乍一看,这些特性是显而易见的。 一对一对应类型的其他属性已经成为讨论的主题,并且并不总是以具体的表示形式出现。 表示数字本身的主题非常引人入胜;对于Knut(第二卷),它已被完全披露,因此它超出了深度,我们深入了整个表述。
假设比特集的长度为n(我们将它们从0到n-1连续编号),并以2的步长进行均匀加权,而最低有效位(数字为0)的权重为1(通常来说根本没有必要,我们只是我们已经习惯了这种情况,并且对我们来说似乎很明显),我们得到了数字的二进制表示,其中的约简公式如下所示:以位集
(2) = (0)*2^0 + (1)*2^1 + ... + (-1)*2^(-1)
或级联形式
2() = (0)+2*((1)+2*(...+2*((-1))..)))
,此后,B(k)表示一个具有数字k的位。 一个不同的观点并没有对数字字节在内存中的位置施加任何限制,但是将低字节放置在较低地址中会更合乎逻辑(这是我很自然而又自然地解决“斯拉夫人的永恒论点”,即哪一端更容易破蛋)。
通过对长度为n(= 8)的一组位的这种解释,我们得到了从0到(2 ^ n)-1(= 255)的数字的表示形式(此后在括号中将有一组8位的特定值),其中有很多和有用的属性,这就是为什么它变得普遍的原因。 不幸的是,它也有许多弊端,其中之一是我们在原则上不能代表负数。
您可以为该问题提供各种解决方案(负数的表示),其中也有实际意义,下面列出了它们。
带有偏移的表示形式由公式H = N2(n)-偏移(C)描述,其中N2是用n位二进制表示法获得的数字,C是某个预选值。 然后我们表示从0-C到2 ^(n)-1-C的数字,如果我们选择C = 2 ^(n-1)-1(= 127)(这是完全可选的,但非常方便),则我们得到的范围是0-(2 ^(n-1)-1)(=-127)到2 ^(n-1)(= 128)。 这种表示的主要优点是整个时间间隔上的单调性(此外,增加),也有一些缺点,其中我们强调了不对称性(还有其他一些与在此表示中对数字执行运算的复杂性有关),但是IEEE 457标准的开发者(这是用于PT)将此缺陷变成了一种美德(使用额外的值对情况进行编码),这再次强调了很酷的说法的忠诚:“如果您高于对手,那么这就是您的优势。 如果对手比你高,那么这也是您的优势。”
请注意,由于任意位数的可能组合的总数是偶数(如果您出于宗教原因没有禁止组合),则从根本上无法实现正负可表示数字之间的对称性(或更确切地说,是可以实现的,但在某些附加条件下,该数量可以进一步实现) 。
当一个位(最高有效位)代表数字H的编码符号时,以直接代码的形式表示=(-1)^ B(n-1)* P2(n-1)的范围为0-(2 ^(n-1) -1)(= -127)到2 ^(n-1)-1(= 127)。 有趣的是,我刚刚宣布了对称的基本不可能,在这里显然是-最大可表示的正数等于最小可表示的负数的模数。 通过具有两个零值(00 ... 00和10 ... 00)来实现此结果,这通常被认为是该方法的主要缺点。 这确实是一个缺点,但并不像通常认为的那样可怕,因为还有更多重要的缺点限制了它的使用。
逆代码表示形式,当在直接表示形式中,我们将负数H =(1-B(n-1))* P2(n-1)+ B(n-1)*(2 ^(n -1)-CH2(n-1))-根据定义,您可以得出一个更容易理解的公式H = Ch2(n-1)-B(n-1)*(2 ^(n-1)-1),这使我们能够表示从0-2 ^(n-1)+1(=-127)到2 ^(n-1)-1(= 127)的数字。 可以看出,该表示是位移的,但是位移是逐步变化的,这使得该表示不是单调的。 同样,我们有两个零,这不是很吓人,加法期间发生循环转移的情况要差得多,这在ALU的实现中会产生某些问题。
要消除上一个表示形式的最后一个缺点非常简单,只需将偏移量更改一个就可以了,然后我们得到== 22(n-1)-B(n-1)* 2 ^(n-1),我们可以表示0-2 ^( n-1)(=-128)至2 ^(n-1)-1(= 127)。 不难看出表示形式是不对称的,但零是唯一的。 更有趣的是以下特性,“完全显而易见”,对于加法类型操作不会发生环转移,这是这种编码负数的特定方法的普遍分布的原因(以及其他令人愉悦的功能)。
让我们为各种数字编码方法绘制一个有趣的值表,用H表示值2 ^(n-1)(128)
位 | 00..00 | 11/01 | 10..00 | 11.11 |
---|
高(n) | 0 | H-1(127) | 高(128) | 2 * H-1(255) |
高(n-1) | 0 | H-1(127) | 0 | H-1(127) |
偏移量。 ^ h | -H + 1(-127) | 0 | 1个 | 高(128) |
直达 | 0 | H-1(127) | 0 | -H + 1(-127) |
倒转 | 0 | H-1(127) | -H + 1(-127) | 0 |
加法 | 0 | H-1(127) | -H(-128) | -1 |
好吧,总而言之,我们给出了列出的表示形式的图表,从中可以立即看出它们的优点和缺点(当然,并不是所有让人回想起有趣的格言的东西:``图形表示信息的优点是视觉的,没有其他优点'')。
第四部分-实际解决原始问题(迟到总比没有好)。
小题外话
首先,我想以十六进制格式打印PT(最终我做到了),但是出乎意料/完全出乎意料(我需要替换),我遇到了以下结果。 您认为执行操作符后会打印出什么内容:
printf("%f %x", 1.0,1.0); printf("%f %x",2.0,2.0); printf("%x %d",1.0,1.0); printf("%x %d",2.0,2.0);
,还请注意以下构造及其结果:
printf("%x %x %f",1.0,1.0);
我不会对这种现象“足够聪明”进行解释。
但是,我们如何正确打印PT的十六进制表示形式? 第一个解决方案很明显-联合,但第二个解决方案是针对单行风扇printf(“%x”,*((int *)(&f))); (对于有人被多余的括号冒犯了,我深表歉意,但是我永远也永远不会记住操作的优先级,尤其是考虑到括号不会生成代码,因此我将继续这样做。) 这就是任务的解决方案-我们看到了一个字符串0x45678,它唯一地确定了我们所需的数字,但是这种方式(我们不知道您,我绝对不知道)对这个数字说不清。 我认为Karnal院士本来可以处理此任务,但他可能指出了源代码打孔带中的一个错误,但并不是每个人都那么先进,因此我们将继续。
我们将尝试以更易理解的形式获取信息。
为此,我们返回到PT的格式(在下文中,我仅考虑浮点数),这是一组位,您可以从中提取(根据某些规则)三组位以表示三个数字-符号,尾数(m)和阶数(p),由这些数字编码的所需数字将由以下公式确定:Cs * Chm * Chn。 在这里,符号表示由相应的位集合表示的数字,因此,为了找到所需的数字,我们需要了解从原始位集合中提取这三个集合的定律以及它们各自的编码类型。
在解决这个问题时,我们转向IEEE标准,发现符号是原始集合的一个(高级)位,并且编码Cs =(-1)^ B(0)的公式。 该顺序占用接下来的8个高位,并以127的偏移量写入代码,并表示2的幂,然后Cn = 2 ^(C2(8)-127)。 尾数取下一个23位数字的顺序,代表数字Chm = 1 + Ch2(23)/ 2 ^ 23。
现在我们有了所有必要的数据,我们可以完全解决任务-创建一个带有字符的字符串,在一定的读数下,该字符串将表示一个等于已编码的数字。 为此,我们应该通过简单的操作提取上述数字,然后打印出来,并提供必要的属性。 我们假设我们能够将不超过32位的整数转换为字符串,那么这非常简单。
不幸的是,我们才刚刚开始,因为很少有读者在“ +1.625 * 2 ^ 3”记录中发现不幸的数字,该数字由更常见的十进制“ 13”编码,并在记录“ 1.953125 * 2”中进行猜测。 ^ 9“简单的” 1E3”或“ 1 * 10 ^ 3”或非常熟悉的“ 1000”具有一般人的能力,我绝对不属于他们。 发生的事情很奇怪,因为我们完成了最初的任务,这再次证明了您应该多么谨慎地对待这些配方。 并不是说十进制表示法好于或差于二进制(在这种情况下,deuce是基于度),而是因为我们习惯于使用十进制,因为童年和改造人比程序困难得多,因此我们将给出入门到比较熟悉。
从数学的角度来看,我们有一个简单的运算-有一条记录PT =(-1)^ s * m * 2 ^ n,我们需要将其转换为PT =(-1)s'* m'* 10 ^ n'的形式。 我们对等式,变换并获得(一种可能的选择)解决方案s'= s',m'= m,n'= n * log(2)。 如果我们省略括号,需要乘以一个显式的非理性数字(如果数字合理化就可以做到,但是稍后再讨论),那么问题似乎可以解决,直到我们看到答案为止,因为如果记录像“ +1.953125” * 2 ^ 9“对我们来说似乎是个晦涩难解的记录” + 1.953125 * 10 ^ 2.70927“即使看起来似乎没有比这更糟的了,它的接受程度甚至更低。
— 10 '= * 10^{ * lg(2)}, '= [ * lg(2)], . (1.953125*10^0.7 0927)*10^2=«10*10^2», , , .
, :
- () (lg(2)) ( );
- ( );
- (10) (...);
- (« , ...»).
, , , 1. , * lg(2), «» , =0 ( =/lg(10)). , « », « ». . , , ' = * lg(2) * [lg(2) *256 + 1/2] / 256 , 1/2/77 = 1/144, , 1/100. — , . [4.501]=5, [4.499]=4 , , 0.002/4.5=0.04%, 1/4=25%. , , . , , , , , , .
'=*77/256
, . 24 , 2^-24=2^-4*2^-20=16^-1*(2^10)^-2~(10)^-1*(10^3)^-2=10^-7, 7 . 24 ( ). , 32 ( ) , 100 (256) , .
' = * 10^{ * lg(2)}— 1) , 2) , , , , , . — , , , .
« , , »
q(10^x) = Δ(10^x)/10^x = (10^(x +Δx) — 10^x)/10^x = 10^Δx -1 = 10^(x*qx)-1,
10^(x*qx) >~ 10^(x*0) + (10^(x*0))'*qx = 1 + x*ln(10)*10^(0)*qx = 1+x*ln(10)*qx,
— , , =127, 292 , , .
, 24 32 ( , ), , (*lg(2)) 32 , , 1'292'914'005/2^32. , , (int)((lg(2)*float(2^32))+0.5), 04d104d42, , .
, , , , .
10 0 1 . , , , , , ''=lg(2)*i+(''-lg(2)*i), 2 , ( ), lg(2) 10^'' ( ).
, lg(2) , , . , , , , 10-7 9 , 1+9*2=19 32- , . '=*lg(2) , .
32- 1+19+1=21
— , — , . , — ( ) , , .
— — (2^8=256) ['] ( 10) {'} ( ), . — =*2^=*10^'*(2^/10^')=(*(2^/10^'))*10^'.
256*3 ( 24 , ) + 256*1 ( 10 2) = 1 . 24*24 ( 32*32), .
, ( , ). , , ( 256 10) . , ,
2^-/10^-' = 1/(2^/10^') != 2^/10^',
可惜。 , . , — 18 , , , , 512 . — , , , , .
- , ( ) . ( )
=*2^=*2^(0+1)=*10^'*(2^(0+1)/10^')=*(2^0/10^')*2^1*10^',
0- , 1=-0. , .
— , 0 ? , — 10 . — 32*32, 24 , 8 8 . 256/8*4=32*4=128 — 8 .
0, , 32/2=16 , , ( ) .
, adafruit
const UINT8 Bits[] = {0x01, 0x02, 0x04, 0x08, 0x10, 0x20, 0x40, 0x80}; ... data = data | Bits[n];
, 1 << n AVR . , , .
, , , ( godbolt, , , ) , .
( , 1 )
ldi r18,lo8(4) sbrs r25,1 ldi r18,lo8(1) sbrc r25,0 lsl r18 sbrs r25,2 swap r18
, , 8:7 8 (, , 16 — — « , , , »). — , : « „ 12 “ (» ", , ).
= * 2^=( * [/8]) * 2^(%8) * 10^[/8],
- - . , 32*32(24*24) . 32 10 , ( , ) .
— ,
const uint32_t Data[32] PROGMEM = { 0xF82345,… }
, , , . , , , ( )
#define POWROUD(pow) ((uint8_t)((pow & 0x07)*log(2)+0.5)) #define MULT(pow) (2^pow / 10^POWROUND(pow)) #define MULTRAW(pow) (uint32_t((MULT(pow) << 24) +0.5)) #define BYTEMASK 0xFF #define POWDATA(pow) ((POWROUND(pow) & BYTEMASK)| (MULTRAW(pow) & (~BYTEMASK))) const uint32_t Data[(BYTEMASK/8)+1] = { POWDATA(0x00),POWDATA(0x08), ..POWDATA(0xF8)}
, , , .
, , , , . . :
1.953125*2^9=1.953125*2^(8+1)=1.953125*42949673/256/256/256(2.56)*2*10^2=10*10^2
1000. , , , , , , , .