当95%的情况下的值为0或1时,是否可以对很大的数组进行随机访问优化?


133

是否有可能对非常大的数组进行随机访问进行任何优化(我目前正在使用uint8_t,而我询问的是哪种更好)

uint8_t MyArray[10000000];

当数组中任意位置的值是

  • 95%的情况下为01
  • 4%的情况下, 2
  • 在其他1%的情况下介于3255之间?

那么,有什么比uint8_t数组更好的东西了吗?应该尽可能快地以随机顺序遍历整个阵列,这在RAM带宽上非常繁重,因此当有多个线程同时对不同的阵列执行此操作时,当前整个RAM带宽很快就饱和了。

我问,因为实际上知道几乎所有值(除了5%之外)几乎都是0或1时,拥有如此大的数组(10 MB)感觉非常低效,所以当数组中所有值的95%实际上只需要1位而不是8位,这将使内存使用量减少近一个数量级。感觉必须要有一种内存效率更高的解决方案,该解决方案将大大减少为此所需的RAM带宽,因此也大大加快了随机访问的速度。


36
两位(0/1 /请参阅哈希表)和一个大于1的值的哈希表?
user253751

6
@ user202729取决于什么?对于那些需要像我一样做类似事情的人,我认为这是一个有趣的问题,因此我希望看到更多针对此的通用解决方案,而不是我的代码特有的答案。如果它取决于某事,那么最好给出一个答案来解释其所依赖的东西,以便每个阅读它的人都可以理解是否有针对自己情况的更好解决方案。
JohnAl '18年

7
本质上,您要问的是所谓的稀疏性
Mateen Ulhaq '18

5
需要更多信息...为什么访问是随机的,并且非零值遵循模式吗?
Ext3h

4
@IwillnotexistIdonotexist预计算步骤会很好,但仍应不时修改数组,因此预计算步骤不应太昂贵。
JohnAl

Answers:


155

我想到的一种简单可能性是,在常见情况下,每个值保留一个压缩数组,每个值2位,每个值一个单独的4字节(原始元素索引为24位,实际值为8位,所以(idx << 8) | value))的已排序数组其他的。

查找值时,首先要在2bpp数组(O(1))中进行查找。如果找到0、1或2,则为您想要的值;如果找到3,则意味着您必须在辅助阵列中查找它。在这里,您将执行二进制搜索以查找感兴趣的索引左移8(O(log(n)的n较小,因为它应该是1%),然后从4-中提取值字节的东西。

std::vector<uint8_t> main_arr;
std::vector<uint32_t> sec_arr;

uint8_t lookup(unsigned idx) {
    // extract the 2 bits of our interest from the main array
    uint8_t v = (main_arr[idx>>2]>>(2*(idx&3)))&3;
    // usual (likely) case: value between 0 and 2
    if(v != 3) return v;
    // bad case: lookup the index<<8 in the secondary array
    // lower_bound finds the first >=, so we don't need to mask out the value
    auto ptr = std::lower_bound(sec_arr.begin(), sec_arr.end(), idx<<8);
#ifdef _DEBUG
    // some coherency checks
    if(ptr == sec_arr.end()) std::abort();
    if((*ptr >> 8) != idx) std::abort();
#endif
    // extract our 8-bit value from the 32 bit (index, value) thingie
    return (*ptr) & 0xff;
}

void populate(uint8_t *source, size_t size) {
    main_arr.clear(); sec_arr.clear();
    // size the main storage (round up)
    main_arr.resize((size+3)/4);
    for(size_t idx = 0; idx < size; ++idx) {
        uint8_t in = source[idx];
        uint8_t &target = main_arr[idx>>2];
        // if the input doesn't fit, cap to 3 and put in secondary storage
        if(in >= 3) {
            // top 24 bits: index; low 8 bit: value
            sec_arr.push_back((idx << 8) | in);
            in = 3;
        }
        // store in the target according to the position
        target |= in << ((idx & 3)*2);
    }
}

对于您建议的数组,第一个数组应为10000000/4 = 2500000字节,第二个数组应为10000000 * 1%* 4 B = 400000字节;因此,有2900000个字节,即少于原始数组的三分之一,并且最常使用的部分都保存在内存中,这对于缓存来说应该是很好的(它甚至可以容纳L3)。

如果您需要超过24位寻址,则必须调整“辅助存储”。扩展它的一种简单方法是使用256个元素的指针数组来切换索引的前8位,然后转发到如上所述的24位索引排序数组。


快速基准

#include <algorithm>
#include <vector>
#include <stdint.h>
#include <chrono>
#include <stdio.h>
#include <math.h>

using namespace std::chrono;

/// XorShift32 generator; extremely fast, 2^32-1 period, way better quality
/// than LCG but fail some test suites
struct XorShift32 {
    /// This stuff allows to use this class wherever a library function
    /// requires a UniformRandomBitGenerator (e.g. std::shuffle)
    typedef uint32_t result_type;
    static uint32_t min() { return 1; }
    static uint32_t max() { return uint32_t(-1); }

    /// PRNG state
    uint32_t y;

    /// Initializes with seed
    XorShift32(uint32_t seed = 0) : y(seed) {
        if(y == 0) y = 2463534242UL;
    }

    /// Returns a value in the range [1, 1<<32)
    uint32_t operator()() {
        y ^= (y<<13);
        y ^= (y>>17);
        y ^= (y<<15);
        return y;
    }

    /// Returns a value in the range [0, limit); this conforms to the RandomFunc
    /// requirements for std::random_shuffle
    uint32_t operator()(uint32_t limit) {
        return (*this)()%limit;
    }
};

struct mean_variance {
    double rmean = 0.;
    double rvariance = 0.;
    int count = 0;

    void operator()(double x) {
        ++count;
        double ormean = rmean;
        rmean     += (x-rmean)/count;
        rvariance += (x-ormean)*(x-rmean);
    }

    double mean()     const { return rmean; }
    double variance() const { return rvariance/(count-1); }
    double stddev()   const { return std::sqrt(variance()); }
};

std::vector<uint8_t> main_arr;
std::vector<uint32_t> sec_arr;

uint8_t lookup(unsigned idx) {
    // extract the 2 bits of our interest from the main array
    uint8_t v = (main_arr[idx>>2]>>(2*(idx&3)))&3;
    // usual (likely) case: value between 0 and 2
    if(v != 3) return v;
    // bad case: lookup the index<<8 in the secondary array
    // lower_bound finds the first >=, so we don't need to mask out the value
    auto ptr = std::lower_bound(sec_arr.begin(), sec_arr.end(), idx<<8);
#ifdef _DEBUG
    // some coherency checks
    if(ptr == sec_arr.end()) std::abort();
    if((*ptr >> 8) != idx) std::abort();
#endif
    // extract our 8-bit value from the 32 bit (index, value) thingie
    return (*ptr) & 0xff;
}

void populate(uint8_t *source, size_t size) {
    main_arr.clear(); sec_arr.clear();
    // size the main storage (round up)
    main_arr.resize((size+3)/4);
    for(size_t idx = 0; idx < size; ++idx) {
        uint8_t in = source[idx];
        uint8_t &target = main_arr[idx>>2];
        // if the input doesn't fit, cap to 3 and put in secondary storage
        if(in >= 3) {
            // top 24 bits: index; low 8 bit: value
            sec_arr.push_back((idx << 8) | in);
            in = 3;
        }
        // store in the target according to the position
        target |= in << ((idx & 3)*2);
    }
}

volatile unsigned out;

int main() {
    XorShift32 xs;
    std::vector<uint8_t> vec;
    int size = 10000000;
    for(int i = 0; i<size; ++i) {
        uint32_t v = xs();
        if(v < 1825361101)      v = 0; // 42.5%
        else if(v < 4080218931) v = 1; // 95.0%
        else if(v < 4252017623) v = 2; // 99.0%
        else {
            while((v & 0xff) < 3) v = xs();
        }
        vec.push_back(v);
    }
    populate(vec.data(), vec.size());
    mean_variance lk_t, arr_t;
    for(int i = 0; i<50; ++i) {
        {
            unsigned o = 0;
            auto beg = high_resolution_clock::now();
            for(int i = 0; i < size; ++i) {
                o += lookup(xs() % size);
            }
            out += o;
            int dur = (high_resolution_clock::now()-beg)/microseconds(1);
            fprintf(stderr, "lookup: %10d µs\n", dur);
            lk_t(dur);
        }
        {
            unsigned o = 0;
            auto beg = high_resolution_clock::now();
            for(int i = 0; i < size; ++i) {
                o += vec[xs() % size];
            }
            out += o;
            int dur = (high_resolution_clock::now()-beg)/microseconds(1);
            fprintf(stderr, "array:  %10d µs\n", dur);
            arr_t(dur);
        }
    }

    fprintf(stderr, " lookup |   ±  |  array  |   ±  | speedup\n");
    printf("%7.0f | %4.0f | %7.0f | %4.0f | %0.2f\n",
            lk_t.mean(), lk_t.stddev(),
            arr_t.mean(), arr_t.stddev(),
            arr_t.mean()/lk_t.mean());
    return 0;
}

(代码和数据始终在我的Bitbucket中更新)

上面的代码使用在其帖子中指定的OP分配的随机数据填充10M元素数组,初始化我的数据结构,然后:

  • 使用我的数据结构执行10M元素的随机查找
  • 通过原始数组执行相同操作。

(请注意,在顺序查找的情况下,阵列始终会在很大程度上赢得胜利,因为它是您可以进行的最便于缓存的查找)

最后两个块重复50次并定时;最后,计算并打印每种类型的查询的平均值和标准差,并显示提速率(lookup_mean / array_mean)。

-O3 -static在Ubuntu 16.04 上使用g ++ 5.4.0(以及一些警告)编译了上面的代码,并在某些计算机上运行了该代码;它们中的大多数运行Ubuntu 16.04,一些较旧的Linux,一些较新的Linux。在这种情况下,我认为操作系统根本不重要。

            CPU           |  cache   |  lookup s)   |     array s)  | speedup (x)
Xeon E5-1650 v3 @ 3.50GHz | 15360 KB |  60011 ±  3667 |   29313 ±  2137 | 0.49
Xeon E5-2697 v3 @ 2.60GHz | 35840 KB |  66571 ±  7477 |   33197 ±  3619 | 0.50
Celeron G1610T  @ 2.30GHz |  2048 KB | 172090 ±   629 |  162328 ±   326 | 0.94
Core i3-3220T   @ 2.80GHz |  3072 KB | 111025 ±  5507 |  114415 ±  2528 | 1.03
Core i5-7200U   @ 2.50GHz |  3072 KB |  92447 ±  1494 |   95249 ±  1134 | 1.03
Xeon X3430      @ 2.40GHz |  8192 KB | 111303 ±   936 |  127647 ±  1503 | 1.15
Core i7 920     @ 2.67GHz |  8192 KB | 123161 ± 35113 |  156068 ± 45355 | 1.27
Xeon X5650      @ 2.67GHz | 12288 KB | 106015 ±  5364 |  140335 ±  6739 | 1.32
Core i7 870     @ 2.93GHz |  8192 KB |  77986 ±   429 |  106040 ±  1043 | 1.36
Core i7-6700    @ 3.40GHz |  8192 KB |  47854 ±   573 |   66893 ±  1367 | 1.40
Core i3-4150    @ 3.50GHz |  3072 KB |  76162 ±   983 |  113265 ±   239 | 1.49
Xeon X5650      @ 2.67GHz | 12288 KB | 101384 ±   796 |  152720 ±  2440 | 1.51
Core i7-3770T   @ 2.50GHz |  8192 KB |  69551 ±  1961 |  128929 ±  2631 | 1.85

结果好坏参半!

  1. 通常,在大多数这些机器上都有某种程度的加速,或者至少它们是同等的。
  2. 在阵列真正胜过“智能结构”查找的两种情况下,它们是在具有大量缓存但并不特别忙的机器上:上面的Xeon E5-1650(15 MB缓存)是一台夜间构建机器,目前相当闲置;Xeon E5-2697(35 MB高速缓存)是一台用于高性能计算的计算机,它也处于空闲状态。确实有道理,原始数组完全适合其巨大的缓存,因此紧凑的数据结构只会增加复杂性。
  3. 在“性能谱”的另一面-但是阵列又稍微快一点,有不起眼的Celeron为我的NAS提供动力。它的缓存非常少,以至于数组和“智能结构”都无法放入其中。缓存足够小的其他计算机也具有类似的性能。
  4. Xeon X5650必须格外小心-它们是非常繁忙的双路虚拟机服务器上的虚拟机;可能是,尽管名义上它具有相当数量的缓存,但是在测试期间,它多次被完全不相关的虚拟机抢占了先机。

7
@JohnAl您不需要结构。A uint32_t会没事的。从辅助缓冲区中删除一个元素显然会使它保持排序状态。插入元素可以先完成,std::lower_bound然后再完成insert(而不是对整个事物进行附加和重新排序)。更新使全尺寸二级阵列更具吸引力-我当然会从这一点开始。
马丁·邦纳

6
@JohnAl因为值是(idx << 8) + val您不必担心值部分-只需使用直接比较即可。它将始终小于((idx+1) << 8) + val和小于((idx-1) << 8) + val
马丁·邦纳

3
@JohnAl:如果可能是有用的,我添加了一个populate应该填充功能main_arrsec_arr根据格式lookup的期望。我实际上没有尝试过,所以不要指望它能真正正常工作:-); 无论如何,它应该给您大致的想法。
意大利Matteo '18

6
我将此+1用作基准测试。很高兴看到有关效率以及多种处理器类型的结果的问题!真好!
杰克·艾德利

2
@JohnAI您应该针对您的实际用例进行概要分析,而没有其他内容。绝尘室的速度无关紧要。
杰克·艾德利

33

另一种选择可能是

  • 检查结果是0、1还是2
  • 如果不进行常规查找

换句话说:

unsigned char lookup(int index) {
    int code = (bmap[index>>2]>>(2*(index&3)))&3;
    if (code != 3) return code;
    return full_array[index];
}

其中bmap每个元素使用2位,值3表示“其他”。

这种结构的更新很简单,使用的内存增加了25%,但是大部分只在5%的情况下被查找。当然,像往常一样,如果一个好主意取决于很多其他条件,那么唯一的答案就是尝试实际使用。


4
我想说这是一个很好的折衷方案,以尽可能多地获取缓存命中(因为简化后的结构可以更轻松地放入缓存中),而又不会浪费很多随机访问时间。
Meneldal '18

我认为这可以进一步改善。过去,我曾经遇到过类似但不同的问题,在利用分支谓词方面有很大帮助,因此获得了成功。将其拆分if(code != 3) return code;if(code == 0) return 0; if(code==1) return 1; if(code == 2) return 2;
kutschkem,

@kutschkem:在这种情况下,__builtin_expect&co或PGO也可以提供帮助。
Matteo Italia

23

这更多的是“冗长的评论”,而不是具体的答案

除非您的数据是众所周知的,否则我怀疑任何人都可以直接回答您的问题(而且我不知道与您的描述相符的任何内容,但是我不知道有关所有类型的所有数据模式的一切种用例)。稀疏数据是高性能计算中的常见问题,但通常是“我们有非常大的数组,但只有一些值是非零的”。

对于像我认为的那样不为人所知的模式,没有人会直接知道哪个更好,这取决于细节:随机访问的随机性是多少-系统是在访问数据项的集群,还是完全随机的?统一的随机数生成器。表数据是完全随机的,还是存在0的序列然后是1的序列,并散布其他值?如果您具有较长的0和1序列,则游程长度编码会很好地工作,但如果“棋盘格为0/1”,则游程长度编码将无法工作。另外,您还必须保留一个“起点”表,以便可以快速地到达相关地点。

我很久以前就知道一些大数据库只是RAM中的一个大表(在此示例中为电话交换订户数据),其中的一个问题是处理器中的缓存和页表优化几乎没有用。呼叫者很少与最近呼叫某人的呼叫者相同,以至于没有任何类型的预加载数据,只是纯随机的。大页表是对该类型访问的最佳优化。

在很多情况下,“速度和小尺寸”之间的折衷是软件工程中必须选择的事情之一(在其他工程中,这不一定是一个折衷的选择)。因此,“浪费内存以简化代码”通常是首选。从这个意义上讲,“简单”解决方案在速度上可能会更好,但是,如果对RAM的使用“更好”,那么优化表的大小将为您提供足够的性能,并在大小上有很好的改进。有很多不同的方法可以实现此目的-如注释中所述,一个2位字段存储两个或三个最常用的值,然后为其他值提供一些替代的数据格式-哈希表将是我的第一种方法,但列表或二叉树也可以工作-再次,这取决于您的“非0、1或2”所在的模式。同样,这取决于表中值的“分散”方式-它们是成簇还是更均匀分布?

但是,这样做的问题是您仍在从RAM读取数据。然后,您将花费更多的代码来处理数据,包括一些代码来应对“这不是一个共同的价值”。

最常见的压缩算法的问题在于它们基于拆包序列,因此您不能随机访问它们。一次将大数据分成256个条目的块,然后将256个解压缩为uint8_t数组,获取所需数据,然后丢弃未压缩的数据的开销极不可能给您带来好处性能-当然,假设这很重要。

最后,您可能必须在评论/答案中实施一个或几个想法以进行测试,看看它是否有助于解决您的问题,或者内存总线是否仍是主要限制因素。


谢谢!最后,我只是对100%的CPU忙于循环访问此类数组(不同数组上的不同线程)时的速度感兴趣。当前,对于一个uint8_t阵列,在同时运行约5个线程(在四通道系统上)后,RAM带宽已饱和,因此使用5个以上的线程不再有任何好处。我希望使用> 10个线程而不会遇到RAM带宽问题,但是如果访问的CPU端变得如此缓慢,以至于10个线程比之前的5个线程做得少,那显然不会取得进展。
JohnAl

@JohnAl您有几个核心?如果您受CPU的限制,那么线程多于内核是没有意义的。另外,也许是时候看一下GPU编程了?
马丁·邦纳

@MartinBonner我目前有12个线程。我同意,这可能会在GPU上很好地运行。
JohnAl

2
@JohnAI:如果您只是在多个线程上运行同一个效率低下的进程的多个版本,则总是会看到进度有​​限。设计用于并行处理的算法将比调整存储结构获得更大的胜利。
杰克·艾德利

13

我过去所做的是在位集前面使用哈希图。

与Matteo的答案相比,此空间减少了一半,但如果“例外”查询速度较慢(即,有许多例外情况),则速度可能会变慢。

但是,通常,“缓存为王”。


2
与Matteo的答案相比,哈希图如何将空间缩小一半?该哈希图中应该是什么?
JohnAl

1
@JohnAl使用1位bitset = bitvec而不是2位bitvec。
o11c

2
@ o11c我不确定我是否理解正确。你的意思是有1个值的数组,其中0的手段main_arr1手段sec_arr(在Matteos代码的情况下)?但是,这将需要比Matteos回答更多的空间,因为它是一个额外的数组。与Matteos的答案相比,我不太了解如何仅使用一半的空间来实现。
JohnAl

1
你能澄清一下吗?你查查expectional情况下第一,并随后在该位图看?如果是这样,我怀疑散列中的缓慢查找会淹没减少位图大小的节省。
马丁·邦纳

我以为这被称为哈希链接-但是Google并没有找到任何相关的热门歌曲,因此它一定是其他东西。通常,它的工作方式是说一个字节数组,该字节数组可保存绝大多数值,例如介于0..254之间。然后,您将使用255作为标志,如果您有255个元素,则将在关联的哈希表中查找true值。有人可以记住它的名字吗?(我想我是在旧的IBM TR中读到的。)无论如何,您也可以按照@ o11c建议的方式进行安排-始终首先在哈希中查找,如果不存在,则在位数组中查找。
davidbak

11

除非您的数据具有模式,否则不太可能出现任何合理的速度或大小优化,并且-假设您以普通计算机为目标,则10 MB并不是什么大问题。

您的问题有两个假设:

  1. 数据存储不佳,因为您没有使用所有位
  2. 更好地存储它可以使事情更快。

我认为这两个假设都是错误的。在大多数情况下,存储数据的适当方法是存储最自然的表示形式。在您的情况下,这就是您所追求的:0到255之间的数字的字节。任何其他表示形式都会更复杂,因此-在所有其他条件相同的情况下-速度更慢,更容易出错。要偏离这一一般原则,您需要一个更强大的理由,而不是需要对95%的数据使用六个“浪费”的位。

对于您的第二个假设,当且仅当更改数组的大小导致缓存未命中率大大降低时,这才是正确的。是否会发生这种情况只能通过对工作代码进行概要分析来确定,但我认为几乎不可能产生重大变化。因为在任何一种情况下您都将随机访问阵列,所以处理器将很难知道要缓存和保留哪种数据。


8

如果数据和访问均匀地随机分布,则性能可能取决于访问的哪一部分避免外部高速缓存未命中。要进行优化,需要知道可以在缓存中可靠地容纳什么大小的数组。如果您的高速缓存足够大,可以每五个单元容纳一个字节,那么最简单的方法可能是让一个字节保存0-2范围内的五个基三编码值(共有243个5个值的组合,因此)(以字节为单位),以及当base-3值表示“ 2”时将查询的10,000,000字节数组。

如果高速缓存不是那么大,但是每8个单元只能容纳一个字节,那么就不可能使用一个字节值从8个base-3值的所有6,561种可能组合中进行选择,但是由于将0或1更改为2将导致不必要的查找,正确性不需要支持所有6,561。相反,您可以专注于256个最“有用”的值。

尤其是如果0比1更常见,反之亦然,一个好的方法可能是使用217个值来编码包含5个或更少1的0和1的组合,使用16个值来编码xxxx0000到xxxx1111,使用16个值来编码0000xxxx到1111xxxx,一个代表xxxxxxxx。对于可能发现的其他用途,将保留四个值。如果按说明将数据随机分配,则所有查询中的绝大多数将命中仅包含零和一的字节(在所有八组的约2/3中,所有位将为零和一,约占7/8)。那些将具有六个或更少的1位);绝大多数未登陆的字节将包含一个包含四个x的字节,并且有50%的机率降为零或一个。因此,只有大约四分之一的查询需要大数组查找。

如果数据是随机分布的,但是缓存不足以处理每八个元素一个字节,则可以尝试使用此方法,每个字节处理八个以上的项目,但除非对0或1有强烈的偏见,则无需在大数组中进行查找即可处理的值的比例会随着每个字节处理的数量的增加而缩小。


7

我将在@ o11c的答案中添加内容,因为他的措辞可能有点令人困惑。如果需要压缩最后一位和CPU周期,请执行以下操作。

我们将从构建一个平衡的二进制搜索树开始,该树包含5%的“其他”情况。对于每次查找,您都可以快速遍历树:您有10000000个元素:其中5%在树中:因此树数据结构包含500000个元素。以O(log(n))的时间进行计算,可以得到19次迭代。我不是专家,但是我想那里有一些内存有效的实现。让我们猜测一下:

  • 平衡的树,因此可以计算子树的位置(索引不需要存储在树的节点中)。以相同的方式将堆(数据结构)存储在线性内存中。
  • 1个字节值(2到255)
  • 3个字节的索引(10000000需要23位,可容纳3个字节)

总计4个字节:500000 * 4 = 1953 kB。适合缓存!

对于所有其他情况(0或1),可以使用位向量。请注意,您不能将5%的其他情况留给随机访问:1.19 MB。

这两个的组合使用大约3099 MB。使用此技术,您将节省3.08的内存。

但是,这并没有超过@Matteo Italia(使用2.76 MB)的答案,太可惜了。我们还有什么可以做的吗?内存消耗最大的部分是树中索引的3个字节。如果我们可以将其降低到2,则可以节省488 kB,总内存使用量为:2.622 MB,这是较小的!

我们如何做到这一点?我们必须将索引减少到2个字节。同样,10000000需要23位。我们需要能够丢弃7位。我们可以简单地通过将10000000个元素的范围划分为78125个元素的2 ^ 7(= 128)个区域来完成此操作。现在,我们可以为每个区域构建一个平衡树,平均有3906个元素。通过将目标索引简单除以2 ^ 7(或bitshift >> 7),即可选择正确的树。现在,所需的索引存储量可以由剩余的16位表示。请注意,树的长度需要存储一些开销,但这可以忽略不计。还要注意,这种拆分机制减少了遍历树所需的迭代次数,现在减少了7次迭代,因为我们删除了7位:只剩下12次迭代。

请注意,理论上您可以重复该过程以切除下一个8位,但这将需要您创建2 ^ 15平衡树,平均约有305个元素。这将产生2.143 MB的内存,仅需4次迭代即可遍历整个树,与我们开始的19次迭代相比,这是一个相当大的加速。

最后的结论是:这在内存使用方面比2位向量策略略胜一筹,但是要实现它却是艰巨的。但是,如果可以区分是否适合缓存,则值得尝试。


1
英勇的努力!
davidbak

1
尝试以下操作:由于4%的案例的值为2 ...,请创建一组例外案例(> 1)。按照实际例外情况(> 2)所述创建树。如果在集合和树中存在,则在树中使用值;如果存在于set中而不是 tree中,则使用值2,否则(不存在于set中)在位向量中查找。树将仅包含100000个元素(字节)。Set包含500000个元素(但完全没有值)。这会减少尺寸,同时又能证明其增加的成本吗?(100%的查找在集合中; 5%的查找也需要在树中查找。)
davidbak

当您拥有一棵不可变的树时,您总是想使用CFBS排序的数组,因此没有分配给节点,只有数据。
o11c

5

如果仅执行读取操作,最好不要将值分配给单个索引,而是分配给索引间隔。

例如:

[0, 15000] = 0
[15001, 15002] = 153
[15003, 26876] = 2
[25677, 31578] = 0
...

这可以通过结构来完成。如果您喜欢OO方法,那么您可能还想定义与此类似的类。

class Interval{
  private:
    uint32_t start; // First element of interval
    uint32_t end; // Last element of interval
    uint8_t value; // Assigned value

  public:
    Interval(uint32_t start, uint32_t end, uint8_t value);
    bool isInInterval(uint32_t item); // Checks if item lies within interval
    uint8_t getValue(); // Returns the assigned value
}

现在,您只需要遍历间隔列表,并检查索引是否位于其中一个间隔内,该间隔平均可以减少大量的内存使用,但会占用更多的CPU资源。

Interval intervals[INTERVAL_COUNT];
intervals[0] = Interval(0, 15000, 0);
intervals[1] = Interval(15001, 15002, 153);
intervals[2] = Interval(15003, 26876, 2);
intervals[3] = Interval(25677, 31578, 0);
...

uint8_t checkIntervals(uint32_t item)

    for(int i=0; i<INTERVAL_COUNT-1; i++)
    {
        if(intervals[i].isInInterval(item) == true)
        {
            return intervals[i].getValue();
        }
    }
    return DEFAULT_VALUE;
}

如果按减小大小的顺序对间隔进行排序,则会增加早发现要查找的项目的可能性,从而进一步降低平均内存和CPU资源使用率。

您也可以删除所有大小为1的间隔。将相应的值放入地图中,仅在间隔中未找到要查找的项目时检查它们。这也应该提高平均性能。


4
有趣的想法(+1),但我有些怀疑,除非有很多长期的0和/或长期的1,否则它是否可以证明开销是合理的。实际上,您建议使用数据的游程长度编码。在某些情况下可能会很好,但可能不是解决此问题的通用方法。
约翰·科尔曼

对。特别是对于随机访问,几乎可以肯定这比简单数组或unt8_t,即使它占用的内存少得多。
左右约

4

很久很久以前,我只记得...

在大学里,我们有一项任务是加速光线跟踪程序,该程序必须通过算法从缓冲区阵列中反复读取。一位朋友告诉我,请始终使用4字节倍数的RAM读取。所以我将数组从[x1,y1,z1,x2,y2,z2,...,xn,yn,zn]的模式更改为[x1,y1,z1,0,x2,y2,z2 ,0,...,xn,yn,zn,0]。意味着我在每个3D坐标之后添加一个空白字段。经过一些性能测试:更快。长话短说:从RAM的数组中读取4字节的倍数,也可以从正确的起始位置读取。因此,您将读取其中包含搜索索引的小群集,并从cpu中的该小群集读取已搜索的索引。(在您的情况下,您无需插入填充字段,但是概念应该很清楚)

也许其他倍数可能是更新系统中的关键。

我不知道这是否适合您的情况,所以如果不起作用:对不起。如果可以,我很高兴听到一些测试结果。

PS:哦,如果有任何访问模式或附近的访问索引,则可以重用缓存的集群。

PPS:可能是因为倍数更像是16Bytes或类似的东西,但是在很久以前,我才能够确切地记得。


您可能正在考虑高速缓存行,通常为32或64字节,但是由于访问是随机的,因此在这里没有多大帮助。
Surt '18

3

看看这个,您可以拆分数据,例如:

  • 一个被索引并表示值0的位集(std :: vector在这里很有用)
  • 一个被索引并代表值1的位集
  • 一个std :: vector的值2,包含引用该值的索引
  • 其他值的映射(或std :: vector>)

在这种情况下,所有值都会显示到给定的索引,因此您甚至可以删除一个位集,并在其他位中缺少该值的情况下表示该值。

这将为您节省一些内存,尽管这会使最坏的情况变得更糟。您还需要更多的CPU功能来进行查找。

确保测量!


1
1/0的位集。一组二的索引。还有一个稀疏的关联数组。
Red.Wave

那是简短的摘要
JVApen

让OP知道术语,以便他可以搜索每个术语的替代实现。
Red.Wave

2

就像Mats在他的评论答案中提到的那样,很难说出什么是最佳的解决方案,而不必特别知道您拥有什么样的数据(例如,长期运行0,等等),以及您的访问模式如何就像(“随机”是指“遍布整个地方”,还是“不是严格地以完全线性的方式”或“每个值恰好都是随机的,只是随机化的”或...)。

也就是说,我想到了两种机制:

  • 位数组;也就是说,如果只有两个值,则可以将数组的压缩量简化为8倍;如果您有4个值(或“ 3个值+其他所有值”),则可以压缩两倍。这可能只是不值得的麻烦,并且需要基准测试,特别是如果您具有真正的随机访问模式,这些模式会逸出缓存并因此根本不更改访问时间。
  • (index,value)(value,index)表格。即,对于1%的情况,有一个非常小的表,对于5%的情况(可能只需要存储索引,因为它们都具有相同的值),可能有一张表,对于最后两种情况,有一个很大的压缩位数组。所谓“表格”,是指可以相对快速查找的内容。即,可能是哈希,二叉树等,具体取决于您的可用资源和实际需求。如果这些子表适合您的一级/二级缓存,您可能会很幸运。

1

我对C不太熟悉,但是在C ++中,您可以使用unsigned char表示0到255之间的整数。

与需要4字节(32位)的普通int(同样,我来自JavaC ++世界)相比,无符号char需要1字节(8位)。因此可能会将阵列的总大小减少75%。


使用uint8_t - 可能已经是这种情况-8表示8位。
彼得·莫滕森

-4

您已经简要地描述了阵列的所有分布特征。扔数组

您可以使用随机方法轻松替换该数组,该方法会产生与数组相同的概率输出。

如果一致性很重要(对于相同的随机索引产生相同的值),请考虑使用布隆过滤器和/或哈希图来跟踪重复命中。但是,如果您的数组访问确实是随机的,则完全没有必要。


18
我怀疑这里使用“随机访问”来表示访问是不可预测的,而不是实际上是随机的。(即从“随机访问文件”的意义上讲)
迈克尔·凯

是的,这很可能。但是,OP尚不清楚。如果OP的访问方式不是随机的,则根据其他答案,将指示某种形式的稀疏数组。
Dúthomhas

1
我认为您已经说了一点,因为OP表示他会以随机顺序遍历整个数组。对于只需要观察分布的情况,这是一个很好的答案。
Ingo Schalk-Schupp,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.