使用 For 循环生成随机视觉噪声
Generating random visual noise using For loop
我开始深入了解 C 语言,使用 arduinos 等,只是想获得一些关于如何使用 For 循环生成随机噪声的建议。
重要的一点:
void testdrawnoise() {
int j = 0;
for (uint8_t i=0; i<display.width(); i++) {
if (i == display.width()-1) {
j++;
i=0;
}
M = random(0, 2); // Random 0/1
display.drawPixel(i, j, M); // (Width, Height, Pixel on/off)
display.refresh();
}
}
该函数在屏幕上一个一个地绘制一个像素,一旦 i
达到 display.width()-1
就向下移动到下一行。像素是亮(黑)还是灭(白)由M
.
决定
代码运行良好,但我觉得它可以做得更好,或者至少更整洁,也许更高效。
非常感谢输入和评论。
首先,您的循环永远不会结束,并且会无限制地递增 j
,因此,在您填满屏幕一次后,您会继续在屏幕高度之外循环;尽管您的库 does bounds checking,但是 CPU 在 j
溢出并返回到零之前继续循环而不实际做有用的工作肯定不是有效的使用。
此外,有符号溢出在 C++ 中是未定义的行为,所以从技术上讲,你的基础不稳固(我最初认为 Arduino 总是用 -fwrapv
编译,这保证了有符号整数溢出时的回绕,but apparently I was mistaken) .
鉴于您正在使用的库将整个帧缓冲区保存在内存中并在 refresh
调用时将其全部发送,因此在每个像素重新发送它没有多大意义 - 特别是因为帧传输可能是迄今为止这个循环中最慢的部分。因此,您可以将其移出循环。
将其放在一起(加上缓存宽度和高度并使用更简单的 random
重载),您可以将其更改为:
void testdrawnoise() {
int w = display.width(), h = display.height();
for (int j=0; j<h; ++j) {
for (int i=0; i<w; ++i) {
display.drawPixel(i, j, random(2));
}
}
display.refresh();
}
(如果您在 AVR Arduinos 上的屏幕尺寸小于 256,您 可能 通过将所有这些 int
更改为 byte
来获得一些东西,但是不要不要相信我的话)
请注意,这只会执行一次,您可以将其放入 loop()
函数或无限循环中以使其不断生成随机模式。
这是您可以使用提供的界面执行的操作;现在,进入无证领域我们可以走得更快。
如上所述,您使用的库将整个帧缓冲区保存在内存中,以每字节 8 位打包(如预期)在一个名为 sharpmem_buffer
, initialized with a malloc
of the obvious size.
的全局变量中
还应该注意的是,当您在代码中请求一个随机位时,PRNG 会生成一个完整的 31 位随机数,并且只取低位。为什么要浪费所有其他非常好的随机位?
同时,当您调用 drawPixel
时,库会对内存中的相应字节执行一系列布尔运算,以仅设置您要求的位,而不会触及其余位。相当愚蠢,因为你无论如何都要用随机覆盖其他的。
因此,将这两个事实放在一起,我们可以做如下事情:
void testdrawnoise() {
// access the buffer defined in another .cpp
extern byte *sharpmem_buffer;
byte *ptr = sharpmem_buffer; // pointer to current position
// end position
byte *end = ptr + display.width()*display.height()/8;
for (; ptr!=end; ++ptr) {
// store a full byte of random
*ptr = random(256);
}
display.refresh();
}
减去 refresh()
时间,应该 至少 比以前的版本快 8 倍(我实际上期望更多,因为不仅循环的核心执行 1/8 的迭代,但它也更简单 - 除了 random
之外没有函数调用,没有分支,没有对内存的布尔运算。
在 AVR Arduinos 上,唯一可以进一步优化的点可能是 RNG - 我们仍然只使用 31 位中的 8 位(如果它们实际上是 31 位?Arduino 文档像往常一样在提供有用的技术方面很糟糕信息)RNG,所以我们可能会从单个 RNG 调用中生成 3 个字节的随机数,或者如果我们切换到不混淆符号位的手动 LCG,则生成 4 个字节。在 ARM Arduinos 上,在最后一种情况下,我们甚至可以通过在内存中执行完整的 32 位存储而不是写入单个字节来获得一些东西。
但是,这些进一步的优化是 (1) 编写起来很乏味(如果你必须处理像素数不是 24/32 的倍数的屏幕)和 (2) 可能不是特别有利可图, 考虑到无论如何大部分时间都将花费在通过 SPI 的传输上。无论如何都值得一提,因为它们可能在其他没有传输瓶颈的情况下有用。
鉴于 OP 的 MCU 实际上是一个 Cortex M0(因此,一个 32 位 ARM),值得尝试使用完整的 32 位 PRNG 和 32 位存储来使其更快。
如上所说,内置了random
returns一个带符号的值,具体提供的范围不是很清楚;出于这个原因,我们将不得不推出我们自己的 PRNG,保证提供 32 个完整的随机位。
提供 32 个随机位和最小状态的体面且非常快速的 PRNG 是 xorshift;我们将直接使用维基百科的 xorshift32,因为我们真的不需要改进的“*”或“+”版本(我们也不真正关心由较大的同行提供的更大的周期)。
struct XorShift32 {
uint32_t state = 0x12345678;
uint32_t next() {
uint32_t x = state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
state = x;
return x;
}
};
XorShift32 xorShift;
现在我们可以改写 testdrawnoise()
:
void testdrawnoise() {
int size = display.width()*display.height();
// access the buffer defined in another .cpp
extern byte *sharpmem_buffer;
/*
we can access the framebuffer as if it was an array of 32-bit words;
this is fine, since it was alloc-ed with malloc, which guarantees memory
aligned for the most restrictive built-in type, and the library only
uses it with byte pointers, so there should be no strict aliasing problem
*/
uint32_t *ptr = (uint32_t *)sharpmem_buffer;
/*
notice that the division is an integer division, which truncates; so, we
are filling the framebuffer up the the last multiple of 4 bytes; with
"strange" sizes we may be leaving out up to 3 bytes (see later)
*/
uint32_t *end = ptr + size/32;
for (; ptr!=end; ++ptr) {
// store a full byte of random
*ptr = xorShift.next();
}
// now to fill the possibly missing last three bytes
// pick it up where we left it
byte *final_ptr = (byte *)end;
byte *final_end = sharpmem_buffer + size/8;
// generate 32 random bits; it's ok, we'll need at most 24
uint32_t r = xorShift.next();
for(; final_ptr!=final_end; ++final_ptr) {
// take the lower 8 bits
*final_ptr = r;
// throw away the bits we used, get in the upper ones
r = r>>8;
}
display.refresh();
}
我开始深入了解 C 语言,使用 arduinos 等,只是想获得一些关于如何使用 For 循环生成随机噪声的建议。 重要的一点:
void testdrawnoise() {
int j = 0;
for (uint8_t i=0; i<display.width(); i++) {
if (i == display.width()-1) {
j++;
i=0;
}
M = random(0, 2); // Random 0/1
display.drawPixel(i, j, M); // (Width, Height, Pixel on/off)
display.refresh();
}
}
该函数在屏幕上一个一个地绘制一个像素,一旦 i
达到 display.width()-1
就向下移动到下一行。像素是亮(黑)还是灭(白)由M
.
代码运行良好,但我觉得它可以做得更好,或者至少更整洁,也许更高效。
非常感谢输入和评论。
首先,您的循环永远不会结束,并且会无限制地递增 j
,因此,在您填满屏幕一次后,您会继续在屏幕高度之外循环;尽管您的库 does bounds checking,但是 CPU 在 j
溢出并返回到零之前继续循环而不实际做有用的工作肯定不是有效的使用。
此外,有符号溢出在 C++ 中是未定义的行为,所以从技术上讲,你的基础不稳固(我最初认为 Arduino 总是用 -fwrapv
编译,这保证了有符号整数溢出时的回绕,but apparently I was mistaken) .
鉴于您正在使用的库将整个帧缓冲区保存在内存中并在 refresh
调用时将其全部发送,因此在每个像素重新发送它没有多大意义 - 特别是因为帧传输可能是迄今为止这个循环中最慢的部分。因此,您可以将其移出循环。
将其放在一起(加上缓存宽度和高度并使用更简单的 random
重载),您可以将其更改为:
void testdrawnoise() {
int w = display.width(), h = display.height();
for (int j=0; j<h; ++j) {
for (int i=0; i<w; ++i) {
display.drawPixel(i, j, random(2));
}
}
display.refresh();
}
(如果您在 AVR Arduinos 上的屏幕尺寸小于 256,您 可能 通过将所有这些 int
更改为 byte
来获得一些东西,但是不要不要相信我的话)
请注意,这只会执行一次,您可以将其放入 loop()
函数或无限循环中以使其不断生成随机模式。
这是您可以使用提供的界面执行的操作;现在,进入无证领域我们可以走得更快。
如上所述,您使用的库将整个帧缓冲区保存在内存中,以每字节 8 位打包(如预期)在一个名为 sharpmem_buffer
, initialized with a malloc
of the obvious size.
还应该注意的是,当您在代码中请求一个随机位时,PRNG 会生成一个完整的 31 位随机数,并且只取低位。为什么要浪费所有其他非常好的随机位?
同时,当您调用 drawPixel
时,库会对内存中的相应字节执行一系列布尔运算,以仅设置您要求的位,而不会触及其余位。相当愚蠢,因为你无论如何都要用随机覆盖其他的。
因此,将这两个事实放在一起,我们可以做如下事情:
void testdrawnoise() {
// access the buffer defined in another .cpp
extern byte *sharpmem_buffer;
byte *ptr = sharpmem_buffer; // pointer to current position
// end position
byte *end = ptr + display.width()*display.height()/8;
for (; ptr!=end; ++ptr) {
// store a full byte of random
*ptr = random(256);
}
display.refresh();
}
减去 refresh()
时间,应该 至少 比以前的版本快 8 倍(我实际上期望更多,因为不仅循环的核心执行 1/8 的迭代,但它也更简单 - 除了 random
之外没有函数调用,没有分支,没有对内存的布尔运算。
在 AVR Arduinos 上,唯一可以进一步优化的点可能是 RNG - 我们仍然只使用 31 位中的 8 位(如果它们实际上是 31 位?Arduino 文档像往常一样在提供有用的技术方面很糟糕信息)RNG,所以我们可能会从单个 RNG 调用中生成 3 个字节的随机数,或者如果我们切换到不混淆符号位的手动 LCG,则生成 4 个字节。在 ARM Arduinos 上,在最后一种情况下,我们甚至可以通过在内存中执行完整的 32 位存储而不是写入单个字节来获得一些东西。
但是,这些进一步的优化是 (1) 编写起来很乏味(如果你必须处理像素数不是 24/32 的倍数的屏幕)和 (2) 可能不是特别有利可图, 考虑到无论如何大部分时间都将花费在通过 SPI 的传输上。无论如何都值得一提,因为它们可能在其他没有传输瓶颈的情况下有用。
鉴于 OP 的 MCU 实际上是一个 Cortex M0(因此,一个 32 位 ARM),值得尝试使用完整的 32 位 PRNG 和 32 位存储来使其更快。
如上所说,内置了random
returns一个带符号的值,具体提供的范围不是很清楚;出于这个原因,我们将不得不推出我们自己的 PRNG,保证提供 32 个完整的随机位。
提供 32 个随机位和最小状态的体面且非常快速的 PRNG 是 xorshift;我们将直接使用维基百科的 xorshift32,因为我们真的不需要改进的“*”或“+”版本(我们也不真正关心由较大的同行提供的更大的周期)。
struct XorShift32 {
uint32_t state = 0x12345678;
uint32_t next() {
uint32_t x = state;
x ^= x << 13;
x ^= x >> 17;
x ^= x << 5;
state = x;
return x;
}
};
XorShift32 xorShift;
现在我们可以改写 testdrawnoise()
:
void testdrawnoise() {
int size = display.width()*display.height();
// access the buffer defined in another .cpp
extern byte *sharpmem_buffer;
/*
we can access the framebuffer as if it was an array of 32-bit words;
this is fine, since it was alloc-ed with malloc, which guarantees memory
aligned for the most restrictive built-in type, and the library only
uses it with byte pointers, so there should be no strict aliasing problem
*/
uint32_t *ptr = (uint32_t *)sharpmem_buffer;
/*
notice that the division is an integer division, which truncates; so, we
are filling the framebuffer up the the last multiple of 4 bytes; with
"strange" sizes we may be leaving out up to 3 bytes (see later)
*/
uint32_t *end = ptr + size/32;
for (; ptr!=end; ++ptr) {
// store a full byte of random
*ptr = xorShift.next();
}
// now to fill the possibly missing last three bytes
// pick it up where we left it
byte *final_ptr = (byte *)end;
byte *final_end = sharpmem_buffer + size/8;
// generate 32 random bits; it's ok, we'll need at most 24
uint32_t r = xorShift.next();
for(; final_ptr!=final_end; ++final_ptr) {
// take the lower 8 bits
*final_ptr = r;
// throw away the bits we used, get in the upper ones
r = r>>8;
}
display.refresh();
}