字节按位反转算法,在有些算法加密或者一些特殊的场合有着较为重要的应用,其速度也是一个非常关键的应用,比如一个byte变量a = 3,其二进制表示为00000011,进行按位反转后的结果即为11000000,即十进制的192。还有一种常用的应用是int型变量按位反转,其基本的原理和字节反转类似,本文仅以字节反转为例来比较这个算法的实现。

  一种最为传统和直接的算法实现如下:

unsigned char Reverse8U(unsigned char x)
{
x = (x & 0xaa) >> | (x & 0x55) << ;
x = (x & 0xcc) >> | (x & 0x33) << ;
x = (x & 0xf0) >> | (x & 0x0f) << ;
return x;
}

  我们对大数据进行测试,测试的代码如下:

void Byte_Reverse_01(unsigned char *Src, unsigned char *Dest, int Length)
{
for (int Y = ; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  当Length=100000000(一亿)时,上面的代码大概用时470毫秒,我们稍微更改下函数的样式,更改如下:

unsigned char Reverse8U(unsigned int x)
{
x = (x & 0xaa) >> | (x & 0x55) << ;
x = (x & 0xcc) >> | (x & 0x33) << ;
x = (x & 0xf0) >> | (x & 0x0f) << ;
return x;
}

  还是使用Byte_Reverse_01的代码,神奇的结果显示速度一下子就跳到220ms,快了一倍多。其实这个看下反汇编的代码就可以看到问题所在了,主要是前面的代码使用了寄存器的低位,在32位的环境下不是很有效。

  注意C语言中默认是传值,所以在函数体内改变了x变量的值,并不会产生其他的什么问题,直接返回这个x不会影响Src中的数据。

  第二步改动,我们试着4路并行看看,即如下代码:

void Byte_Reverse_02(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
Dest[Y + ] = Reverse8U(Src[Y + ]);
Dest[Y + ] = Reverse8U(Src[Y + ]);
Dest[Y + ] = Reverse8U(Src[Y + ]);
Dest[Y + ] = Reverse8U(Src[Y + ]);
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  四路并行,一个是可以让编译器编译出能更充分利用指令级并行的指令(即在同一个指令周期内完成2个或多个指令),二是在一定程度上减少了循环变量计数的耗时,虽然这个对大循环不明显,但是在本例这种轻计算量的代码里还是有一定作用的。

  算法的速度变为大概195ms,提速不是很明显。

  下一步改进,我们知道,现代编译器对字节变量的处理其实速度可能还不如处理int类型,因此,我们考虑把这个四个字节的反转用一个int类型的变量也一次性实现,这可以用下面的代码实现:

unsigned int Reverse32I(unsigned int x)
{
x = (((x & 0xaaaaaaaa) >> ) | ((x & 0x55555555) << ));
x = (((x & 0xcccccccc) >> ) | ((x & 0x33333333) << ));
x = (((x & 0xf0f0f0f0) >> ) | ((x & 0x0f0f0f0f) << ));
return x;
}

  注意这里起名叫Reverse32I其实不是很适合,毕竟他不是反转32位数,但你知道就可以了。

  测试代码如下:

void Byte_Reverse_03(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
*((unsigned int *)(Dest + Y)) = Reverse32I(*((unsigned int *)(Src + Y)));
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  测试结果显示执行耗时为65ms,靠,速度提高了三四倍。

  接下来,我们考虑另外的实现方法,因为byte只有256个不同的数,因此,我们也可以直接用查表的方式来实现,这个表可以实时计算(耗时可以忽视),也可以静态给出,前人已经给给出了,这里我直接贴出来:

static const unsigned char BitReverseTable256[] =
{
0x00, 0x80, 0x40, 0xC0, 0x20, 0xA0, 0x60, 0xE0, 0x10, 0x90, 0x50, 0xD0, 0x30, 0xB0, 0x70, 0xF0,
0x08, 0x88, 0x48, 0xC8, 0x28, 0xA8, 0x68, 0xE8, 0x18, 0x98, 0x58, 0xD8, 0x38, 0xB8, 0x78, 0xF8,
0x04, 0x84, 0x44, 0xC4, 0x24, 0xA4, 0x64, 0xE4, 0x14, 0x94, 0x54, 0xD4, 0x34, 0xB4, 0x74, 0xF4,
0x0C, 0x8C, 0x4C, 0xCC, 0x2C, 0xAC, 0x6C, 0xEC, 0x1C, 0x9C, 0x5C, 0xDC, 0x3C, 0xBC, 0x7C, 0xFC,
0x02, 0x82, 0x42, 0xC2, 0x22, 0xA2, 0x62, 0xE2, 0x12, 0x92, 0x52, 0xD2, 0x32, 0xB2, 0x72, 0xF2,
0x0A, 0x8A, 0x4A, 0xCA, 0x2A, 0xAA, 0x6A, 0xEA, 0x1A, 0x9A, 0x5A, 0xDA, 0x3A, 0xBA, 0x7A, 0xFA,
0x06, 0x86, 0x46, 0xC6, 0x26, 0xA6, 0x66, 0xE6, 0x16, 0x96, 0x56, 0xD6, 0x36, 0xB6, 0x76, 0xF6,
0x0E, 0x8E, 0x4E, 0xCE, 0x2E, 0xAE, 0x6E, 0xEE, 0x1E, 0x9E, 0x5E, 0xDE, 0x3E, 0xBE, 0x7E, 0xFE,
0x01, 0x81, 0x41, 0xC1, 0x21, 0xA1, 0x61, 0xE1, 0x11, 0x91, 0x51, 0xD1, 0x31, 0xB1, 0x71, 0xF1,
0x09, 0x89, 0x49, 0xC9, 0x29, 0xA9, 0x69, 0xE9, 0x19, 0x99, 0x59, 0xD9, 0x39, 0xB9, 0x79, 0xF9,
0x05, 0x85, 0x45, 0xC5, 0x25, 0xA5, 0x65, 0xE5, 0x15, 0x95, 0x55, 0xD5, 0x35, 0xB5, 0x75, 0xF5,
0x0D, 0x8D, 0x4D, 0xCD, 0x2D, 0xAD, 0x6D, 0xED, 0x1D, 0x9D, 0x5D, 0xDD, 0x3D, 0xBD, 0x7D, 0xFD,
0x03, 0x83, 0x43, 0xC3, 0x23, 0xA3, 0x63, 0xE3, 0x13, 0x93, 0x53, 0xD3, 0x33, 0xB3, 0x73, 0xF3,
0x0B, 0x8B, 0x4B, 0xCB, 0x2B, 0xAB, 0x6B, 0xEB, 0x1B, 0x9B, 0x5B, 0xDB, 0x3B, 0xBB, 0x7B, 0xFB,
0x07, 0x87, 0x47, 0xC7, 0x27, 0xA7, 0x67, 0xE7, 0x17, 0x97, 0x57, 0xD7, 0x37, 0xB7, 0x77, 0xF7,
0x0F, 0x8F, 0x4F, 0xCF, 0x2F, 0xAF, 0x6F, 0xEF, 0x1F, 0x9F, 0x5F, 0xDF, 0x3F, 0xBF, 0x7F, 0xFF
};

  最直接的查找代码如下:

void Byte_Reverse_04(unsigned char *Src, unsigned char *Dest, int Length)
{
for (int Y = ; Y < Length; Y++)
{
Dest[Y] = BitReverseTable256[Src[Y]];
}
}

  测试耗时:70ms,速度也是不错的。

  如果分四路并行测试,代码如下:

void Byte_Reverse_05(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
Dest[Y + ] = BitReverseTable256[Src[Y + ]];
Dest[Y + ] = BitReverseTable256[Src[Y + ]];
Dest[Y + ] = BitReverseTable256[Src[Y + ]];
Dest[Y + ] = BitReverseTable256[Src[Y + ]];
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = BitReverseTable256[Src[Y]];
}
}

  测试耗时:40ms,速度又有了大幅度的提高了。同时和前面的Byte_Reverse_02相比,明天提速比例完全不在一个档次,这是因此这里代码非常简单,就是一个查找表,他很容易实现指令级的并行。

  还有一种方式,其实也类似于四路并行,如下所示:

void Byte_Reverse_06(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
unsigned int Value = *((unsigned int *)(Src + Y));
*((unsigned int *)(Dest + Y)) = (BitReverseTable256[Value & 0xff]) |
(BitReverseTable256[(Value >> ) & 0xff] << ) |
(BitReverseTable256[(Value >> ) & 0xff] << ) |
(BitReverseTable256[(Value >> ) & 0xff] << );
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = BitReverseTable256[Src[Y]];
}
}

  本来想利用int比byte处理起来快的特性,但是这样处理有计算量增大了,结果耗时50ms,比四路并行反而慢了一点。

  在c语言实现bit反转的最佳算法-从msb-lsb到lsb-msb一文的回复一栏中,我无意看到ytfhwfnh的回复如下:

   我觉得查表法不错,但是表太大了,建议改为半字节为单元的查表。这样,只需要16个uchar的表就够了。查表,再翻转高低半字节,再翻转一个int32的4个字节。就能搞定了。

  他这个话的后续的再翻转一个int32的4个字节在本例中正好不要,他提供的示例代码如下:

LOCAL u_long ucharBitsListR2Ulong(u_char* ucBits)
{
const static u_char BitReverseTable16[] =
{
0x0, 0x8, 0x4, 0xC, 0x2, 0xA, 0x6, 0xE,
0x1, 0x9, 0x5, 0xD, 0x3, 0xB, 0x7, 0xF
};
u_long ret = ;
int i = ; for (; i < ; i++)
{
/* 获取当前字节,高4位 */
u_char ucTmp = (ucBits[i] >> ) & 0x0F;
/* 查表得反转的半字节,并转为u_long */
u_long ulTmp = BitReverseTable16[ucTmp];
/* 存入ret对应位置的低4位 */
ret [表情]= ulTmp << (i * ); /* 获取当前字节,低4位 */
ucTmp = ucBits[i] & 0x0F;
/* 查表得反转的半字节,并转为u_long */
ulTmp = BitReverseTable16[ucTmp];
/* 存入ret对应位置的高4位 */
ret [表情]= ulTmp << (i * + );
} return ret;
}

  这个[表情]是 CSDN的特色,实际上他应该是| 运算符。

  我们把他这个函数直接展开嵌入到循环中,形成了如下的利用16位进行查表的算法:

void Byte_Reverse_08(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
unsigned int Value = *((unsigned int *)(Src + Y));
*((unsigned int *)(Dest + Y)) =
(BitReverseTable16[(Src[Y + ] >> ) & 0x0F]) | (BitReverseTable16[Src[Y + ] & 0x0F] << ) |
(BitReverseTable16[(Src[Y + ] >> ) & 0x0F] << ) | (BitReverseTable16[Src[Y + ] & 0x0F] << ) |
(BitReverseTable16[(Src[Y + ] >> ) & 0x0F] << ) | (BitReverseTable16[Src[Y + ] & 0x0F] << ) |
(BitReverseTable16[(Src[Y + ] >> ) & 0x0F] << ) | (BitReverseTable16[Src[Y + ] & 0x0F] << );
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  很可惜,我没有得到我想要的效果,这段代码结果耗时110ms,比256个元素的查找表慢。

  那同样,我们用四路并行实现他们试下,即如下代码:

void Byte_Reverse_08(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
Dest[Y + ] = (BitReverseTable16[(Src[Y + ] >> ) & 0x0F]) | (BitReverseTable16[Src[Y + ] & 0x0F] << );
Dest[Y + ] = (BitReverseTable16[(Src[Y + ] >> ) & 0x0F]) | (BitReverseTable16[Src[Y + ] & 0x0F] << );
Dest[Y + ] = (BitReverseTable16[(Src[Y + ] >> ) & 0x0F]) | (BitReverseTable16[Src[Y + ] & 0x0F] << );
Dest[Y + ] = (BitReverseTable16[(Src[Y + ] >> ) & 0x0F]) | (BitReverseTable16[Src[Y + ] & 0x0F] << );
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  同样道理,这样又要快一点了,能做到75ms,但比256个查找表的多路并行还是要慢的。

  这是可以理解的,一般来说,查找表越少,同样的查找次数耗时则越小,这主要得益于小的查找表有着较小的cache miss,但是我们注意到,上述16个元素的查找表的查找次数多了一倍,而且也多了很多移位和或运算,因此,总的耗时并没有减少。

  但是,到这里,就出现了一个令我非常感兴趣的话题了,我一直在思考如何利用SIMD指令实现快速的查表问题,后来得到的结论是,这个基本上不可行,对应SSE,除非几个特殊的表,一个情况就是,这个查找表只有16个元素,而且表的类型是byte类型,这个时候,我们就可以利用_mm_shuffle_epi8指令进行快速的shuffle,此时的效果就比直接查表要快了很多了。

  那么仔细的观察上面的代码,除了查表之外,其他的计算太容易用SSE相应的指令实现了,或计算,并计算,注意移位计算SSE指令的_mm_srli_si128 、_mm_slli_si128并不是按位移位的,他是按照字节进行的移位,这个时候我们可借用_mm_srli_epi16或者_mm_srli_epi32来实现相同的功能。

  此时,可编制如下的SSE代码实现相同的功能:

void Byte_Reverse_09(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
__m128i Table = _mm_loadu_si128((__m128i *)BitReverseTable16);
__m128i Mask = _mm_set1_epi8(0x0F);
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
__m128i SrcV = _mm_loadu_si128((__m128i *)(Src + Y));
__m128i High = _mm_and_si128(_mm_srli_epi16(SrcV, ), Mask); // 高四位
__m128i Low = _mm_and_si128(SrcV, Mask); // 低四位
High = _mm_shuffle_epi8(Table, High); // 查找表
Low = _mm_shuffle_epi8(Table, Low);
_mm_storeu_si128((__m128i *)(Dest + Y), _mm_or_si128(High, _mm_slli_epi16(Low, ))); // 合并保存
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  此时函数的执行速度提高到了25ms左右,并且我们看到,这里其实是实质上就没有任何的查表工作了,也不存在所谓的cache miss的。

  在此基础上,我们可以将这个函数扩展到使用AVX优化,AVX支持一次性处理32个字节的数据,比SSE还要扩展一倍,并且现在大部分CPU已经支持AVX2了,尝试一下:

void Byte_Reverse_10(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
__m256i Table = _mm256_loadu_si256((__m256i *)BitReverseTable32);
__m256i Mask = _mm256_set1_epi8(0x0F);
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
__m256i SrcV = _mm256_loadu_si256((__m256i *)(Src + Y));
__m256i High = _mm256_and_si256(_mm256_srli_epi16(SrcV, ), Mask); // 高四位
__m256i Low = _mm256_and_si256(SrcV, Mask); // 低四位
High = _mm256_shuffle_epi8(Table, High); // 查找表
Low = _mm256_shuffle_epi8(Table, Low);
_mm256_storeu_si256((__m256i *)(Dest + Y), _mm256_or_si256(High, _mm256_slli_epi16(Low, ))); // 合并保存
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  速度也基本在25ms左右波动,区别和SSE不是很多大。

  最后,我们在返回到最开始的unsigned char Reverse8U(unsigned char x)函数,我们发现,这个函数内部的算法天然的就支持SSE并行化处理,我们可以稍微修改下语法就可以得到对应的SSE版本函数,如下所示:

void Byte_Reverse_11(unsigned char *Src, unsigned char *Dest, int Length)
{
int BlockSize = , Block = Length / BlockSize;
for (int Y = ; Y < Block * BlockSize; Y += BlockSize)
{
__m128i V = _mm_loadu_si128((__m128i *)(Src + Y));
V = _mm_or_si128(_mm_srli_epi16(_mm_and_si128(V, _mm_set1_epi8(0xaa)), ), _mm_slli_epi16(_mm_and_si128(V, _mm_set1_epi8(0x55)), ));
V = _mm_or_si128(_mm_srli_epi16(_mm_and_si128(V, _mm_set1_epi8(0xcc)), ), _mm_slli_epi16(_mm_and_si128(V, _mm_set1_epi8(0x33)), ));
V = _mm_or_si128(_mm_srli_epi16(_mm_and_si128(V, _mm_set1_epi8(0xf0)), ), _mm_slli_epi16(_mm_and_si128(V, _mm_set1_epi8(0x0f)), ));
_mm_storeu_si128((__m128i *)(Dest + Y), V);
}
for (int Y = Block * BlockSize; Y < Length; Y++)
{
Dest[Y] = Reverse8U(Src[Y]);
}
}

  这个版本也是相当的快的,大约用时28ms左右,而且不占用任何其他内存。

  当然,以上的时间比较只基于本人的一台电脑,在不同的CPU系列当中,各算法之间的耗时比例是不太相同的。有些甚至出现了相反的现象,总的来说,用多路并行256个元素的查找表方式是最为稳妥和夸平台的,如果在PC段,则可以考虑时候用SIMD优化的版本。

  各版本的总和速度比较如下:

  附本文完整测试代码供有兴趣的朋友研究:https://files.cnblogs.com/files/Imageshop/Byte_Reverse.rar

  既然是针对字节的数据处理,自然而然我们想到可以直接把他应用在图像上,比如对lena图像,应用这个算法得到如下效果:

 

  后面一幅图你还能看出他是lena吗,但是确实可以对后面的图再次利用本算法,恢复出完整的lena图,这也可以算是最简答的图像加密算法之一吧。

写博不易,欢迎土豪打赏赞助。

【算法随记七】巧用SIMD指令实现急速的字节流按位反转算法。的更多相关文章

  1. SSE图像算法优化系列三十二:Zhang\Guo图像细化算法的C语言以及SIMD指令优化

    二值图像的细化算法也有很多种,比较有名的比如Hilditch细化.Rosenfeld细化.基于索引表的细化.还有Opencv自带的THINNING_ZHANGSUEN.THINNING_GUOHALL ...

  2. 【算法随记】Canny边缘检测算法实现和优化分析。

    以前的博文大部分都写的非常详细,有很多分析过程,不过写起来确实很累人,一般一篇好的文章要整理个三四天,但是,时间越来越紧张,后续的一些算法可能就以随记的方式,把实现过程的一些比较容易出错和有价值的细节 ...

  3. 【算法随记三】小半径中值模糊的急速实现(16MB图7.5ms实现) + Photoshop中蒙尘和划痕算法解读。

    在本人的博客里,分享了有关中值模糊的O(1)算法,详见:任意半径中值滤波(扩展至百分比滤波器)O(1)时间复杂度算法的原理.实现及效果 ,这里的算法的执行时间和参数是无关的.整体来说,虽然速度也很快, ...

  4. 【算法随记六】一段Matlab版本的Total Variation(TV)去噪算法的C语言翻译。

    最近看到一篇文章讲IMAGE DECOMPOSITION,里面提到了将图像分为Texture layer和Structure layer,测试了很多方法,对于那些具有非常强烈纹理的图像,总觉得用TV去 ...

  5. Doris开发手记2:用SIMD指令优化存储层的热点代码

    最近一直在进行Doris的向量化计算引擎的开发工作,在进行CPU热点排查时,发现了存储层上出现的CPU热点问题.于是尝试通过SIMD的指令优化了这部分的CPU热点代码,取得了较好的性能优化效果.借用本 ...

  6. 搜索引擎算法研究专题七:Hilltop算法

    搜索引擎算法研究专题七:Hilltop算法 2017年12月19日 ⁄ 搜索技术 ⁄ 共 1256字 ⁄ 字号 小 中 大 ⁄ 评论关闭   HillTop也是搜索引擎结果排序的专利,是Google工 ...

  7. if-then-else、loop控制语句在SIMD指令下的后端指令生成实现--笔记

    作者:Yaong 出处:https://www.cnblogs.com/yaongtime/p/14111134.html 版权:本文版权归作者和博客园共有 转载:欢迎转载,但未经作者同意,必须保留此 ...

  8. 48 容器(七)——HashMap底层:哈希表结构与哈希算法

    哈希表结构 哈希表是由数组+链表组成的,首先有一个数组,数组的每一个位置都用来存储一个链表,链表的基本节点为:[hash值,key值,value值,next],当存入一个键值对时,首先调用hashco ...

  9. Reverse反转算法+斐波那契数列递归+Reverse反转单链表算法--C++实现

    Reverse反转算法 #include <iostream> using namespace std; //交换的函数 void replaced(int &a,int & ...

随机推荐

  1. 16.libgdx根据配置文件生成布局(未完)

    思路: screen分为普通和复杂两种,普通的功能大部分是页面跳转以及简单的crud数据,复杂的单独弄出来 跳转普通的screen,直接根据配置文件调整设置 <layouts> <l ...

  2. myeclipse 如何更改java jsp 等文件的编码方式

    java的编码方式: 1.window——>preference 2.General——>Workspace,右边[Text file encoding]选择编码后,点击[OK]. jsp ...

  3. javascript简介 标签: javascript 2015-11-13 12:13 1712人阅读 评论(39)

    JavaScript是一种属于网络的脚本语言,已经被广泛用于Web应用开发,常用来为网页添加各式各样的动态功能,为用户提供更流畅美观的浏览效果.通常JavaScript脚本是通过嵌入在HTML中来实现 ...

  4. jQuery学习笔记之可见性过滤选择器

    可见性过滤选择器是根据元素的可见和不可见状态来选择相应的元素. 显示隐藏的例子: <!DOCTYPE html> <html> <head> <script ...

  5. Hbase API: 写入Bigtable.

  6. HDFS概念名称节点和数据节点-名称节点

  7. C++中文本的读入

    读入文本文件 标准库包含逐行读取文本文件的功能.然后,你可以一次一行地解析文本文件的每一行. 比如说,你有文件,其中使用数字和逗号表示一个 3x4 的矩阵: , , , 10.5 , , , , , ...

  8. 模板—v-dcc缩点

    int dfn[MAXN],low[MAXN],num,root; int stack[MAXN],top,cnt; bool iscut[MAXN]; int new_id[MAXN],c[MAXN ...

  9. Bitmap的recycle问题

        虽然Android有自己的垃圾回收机制,对于是不是要我们自己调用recycle,还的看情况而定.如果只是使用少量的几张图片,回收与否关系不大.可是若有大量bitmap需要垃圾回收处理,那必然垃 ...

  10. loadrunner_关联检查点参数化

    1.保存变量函数 lr_save_string("192.168.xx.xx","ip"); URL=http://{ip} 2.检查点函数 web_reg_f ...