我想在运行时构建位图。位图应在所有方面都可扩展,像素访问应安静高效。
一些插图http://img546.imageshack.us/img546/4995/maptm.jpg
在图片所示命令之间以及之后,Map.setPixel()和Map.getPixel()应当设置/返回保存在位图中的数据。
我不希望实现只是一个概念,即如何以setPixel()/ getPixel尽可能快的方式分配内存。
extendX
方法以加快setPixel
和getPixel
的速度?
我想在运行时构建位图。位图应在所有方面都可扩展,像素访问应安静高效。
一些插图http://img546.imageshack.us/img546/4995/maptm.jpg
在图片所示命令之间以及之后,Map.setPixel()和Map.getPixel()应当设置/返回保存在位图中的数据。
我不希望实现只是一个概念,即如何以setPixel()/ getPixel尽可能快的方式分配内存。
extendX
方法以加快setPixel
和getPixel
的速度?
Answers:
如果extend()
操作需要相当快,那么四叉树可能是一个很好的选择。实际上,它甚至不需要显式的扩展操作。诚然,对于随机访问单个像素而言,它不会产生最佳性能,但是您的评论说,您的主要操作是在像素上进行迭代,四叉树可以非常快地完成,也许几乎与基于矩阵的实现一样快(并且更快)如果迭代并非总是以矩阵布局的相同方式发生)。
您的要求实际上听起来像是您正在尝试实现像生命游戏这样的细胞自动机。您可能想看看Hashlife,这是一种在无限网格上实施“人生游戏”的极高性能方法。请注意,它基于四叉树,但是会根据游戏规则的位置进行一些非常聪明的附加优化。
如果内存不是非常稀疏的资源,我考虑使用更大的块。
这是一些伪代码。
class Chunk {
Chunk new(int size) {...}
void setPixel(int x, int y, int value) {...}
int getPixel(int x, int y) {...}
}
class Grid {
Map<int, Map<Chunk>> chunks;
Grid new(int chunkSize) {...}
void setPixel(int x, int y, int value) {
getChunk(x,y).setPixel(x % chunkSize, y % chunkSize, value);//actually the modulo could be right in Chunk::setPixel and getPixel for more safety
}
int getPixel(int x, int y) { /*along the lines of setPixel*/ }
private Chunk getChunk(int x, int y) {
x /= chunkSize;
y /= chunkSize;
Map<Chunk> row = chunks.get(y);
if (row == null) chunks.set(y, row = new Map<Chunk>());
Chunk ret = row.get(x);
if (ret == null) row.set(x, ret = new Chunk(chunkSize));
return ret;
}
}
这种实现是很幼稚的。
首先,它在getPixel中创建块(基本上,如果没有为该位置定义块,则简单地返回0左右就可以了)。其次,它基于这样的假设,即您具有足够快速和可伸缩的Map实现。据我所知,每种体面的语言都有一种。
另外,您还必须使用块大小。对于密集位图,较大的块大小是好的,对于稀疏位图,较小的块大小更好。实际上,对于非常稀疏的像素来说,“块大小”为1是最好的,这会使“块”本身过时,并将数据结构缩小为像素的int映射的int映射。
另一个解决方案可能是查看某些图形库。实际上,他们非常擅长将一个2D缓冲区绘制到另一个2D缓冲区中。这意味着您只需分配一个更大的缓冲区,然后在相应的坐标处将原始缓冲区拖入其中。
作为一般策略:当有一个“动态增长的内存块”时,一旦使用完,最好分配它的倍数。这是相当耗费内存的,但是可以大大减少分配和复制成本。大多数矢量实现分配的大小超过其两倍。因此,特别是如果您使用现成的解决方案,请不要将缓冲区仅扩展1个像素,因为只请求了一个像素。分配的内存很便宜。重新分配,复制和释放非常昂贵。
几点建议:
如果将其实现为某种整数类型的数组(或...的数组),则可能应每次将后备数组的位数增加一些位数/像素,以避免在复制它们时移位。不利的一面是您使用了更多的空间,但是随着位图变大,浪费的空间比例下降了。
如果使用基于Map的数据结构,则可以通过简单地重新定位getPixel
and setPixel
调用的x,y坐标参数来解决增长位图的问题。
如果您使用基于地图的数据结构,则只需要“一个”的地图条目。可以通过缺少条目来指示“零”。这样可以节省大量空间,尤其是在位图大部分为零的情况下。
您不需要使用“地图地图”。您可以将int
x,y对编码为单个long
。可以使用类似的过程将数组的数组映射到数组。
最后,您需要权衡3件事:
getPixel
和setPixel
,extend*
运营绩效,以及进行像素访问的绝对最快的方法是分别寻址像素的二维阵列。
对于扩展,请从一个简单的实现开始,该实现每次都会重新分配和复制(因为无论如何您都需要该代码)。如果剖析不表示您花费大量时间进行处理,则无需进一步完善。
如果分析显示有必要减少重新分配的次数,并且您没有受到内存的限制,请考虑在每个方向上按一定百分比过度分配并存储与原点的偏移量。(例如,如果您从1x1开始一个新的位图并分配一个9x9数组来保存它,则初始值x
和y
偏移量将是4
。)这里的权衡是在像素访问期间必须进行额外的数学运算才能应用偏移量。
如果扩展真的很昂贵,则可以尝试以下一种或两种方法:
以不同的方式处理垂直和水平扩展。可以通过分配一个新块并将整个现有数组的单个副本复制到新内存中的适当偏移位置来实现在任何方向上垂直扩展数组。将其与水平扩展相比较,在水平扩展中,每行必须执行一次该操作,因为现有数据在新块中不是连续的。
跟踪最频繁的延伸量和延伸方向。使用该信息来选择新的大小和偏移量,这将减少必须为任何扩展名进行重新分配和复制的可能性。
就个人而言,除非像素访问扩展比很低,否则我是否会需要其中之一。
Map.setPixel()
并Map.getPixel()
一次修改单个像素,还提供了一次复制和修改一个像素矩形的方法。这将使呼叫者可以根据呼叫者可用的信息来选择更有效的访问方式。
(让我们不支持热闹的答案...)
最灵活,也许最可靠的实现是一个链表,该链表的结构给出x坐标,y坐标和位值。我会先构建它并使它工作。
然后,如果速度太慢和/或太大,请尝试加快速度的常用方法:数组,矩阵,位图,压缩,缓存,取反,仅存储“ 1”值等。
与修正错误的快速实现相比,使缓慢的正确实现更快变得容易。在测试“快速”的第二种实现时,您有一个参照标准可以与之进行比较。
而且,谁知道,您可能会发现慢速版本足够快。只要整个结构适合内存,事情就已经非常快了。
如果您实际上需要任意大小的位图-我的意思是从1x1到1000000x1000000 amd,并且需要按需扩展...一种可能的方法是使用数据库。乍一看似乎很不直观,但是您真正拥有的是存储问题。数据库将使您能够访问单个像素并本质上存储任何数量的数据。我不一定是指SQL数据库,顺便说一句。
它会足够快达到您的目的吗?我无法回答,因为这里没有关于您使用此位图的操作的上下文。但是,如果这是用于屏幕显示,请考虑到您通常只需要拉回其他扫描行即可在屏幕滚动时显示,而不是所有数据。
话虽这么说,我不禁想知道您是否要解决某些问题。您是否应该改为使用基于矢量的图形并跟踪内存中的各个实体,然后仅将渲染的位图渲染为屏幕所需的大小?
以下是使其正常运行的步骤:
示例实现为:
template<class T, class P>
class ExtendBottom {
public:
ExtendBottom(T &t, int count) : t(t), count(count),k(t.XSize(), count) { }
P &At(int x, int y) const { if (y<t.YSize()) return t.At(x,y); else return k.At(x, y-t.YSize()); }
int XSize() const { return t.XSize(); }
int YSize() const { return t.YSize()+count; }
private:
T &t;
int count;
MemoryBitmap k;
};
显然,对于真正的实现,XSize()和YSize()无效,但是您需要MinX(),MaxX(),MinY(),MaxY()或类似的东西来保持索引号的一致性。