Quickly find whether a value is present in a C array?

前端 未结 15 1222
灰色年华
灰色年华 2021-01-29 17:30

I have an embedded application with a time-critical ISR that needs to iterate through an array of size 256 (preferably 1024, but 256 is the minimum) and check if a value matches

相关标签:
15条回答
  • 2021-01-29 17:49

    Use a hash set. It will give O(1) lookup time.

    The following code assumes that you can reserve value 0 as an 'empty' value, i.e. not occurring in actual data. The solution can be expanded for a situation where this is not the case.

    #define HASH(x) (((x >> 16) ^ x) & 1023)
    #define HASH_LEN 1024
    uint32_t my_hash[HASH_LEN];
    
    int lookup(uint32_t value)
    {
        int i = HASH(value);
        while (my_hash[i] != 0 && my_hash[i] != value) i = (i + 1) % HASH_LEN;
        return i;
    }
    
    void store(uint32_t value)
    {
        int i = lookup(value);
        if (my_hash[i] == 0)
           my_hash[i] = value;
    }
    
    bool contains(uint32_t value)
    {
        return (my_hash[lookup(value)] == value);
    }
    

    In this example implementation, the lookup time will typically be very low, but at the worst case can be up to the number of entries stored. For a realtime application, you can consider also an implementation using binary trees, which will have a more predictable lookup time.

    0 讨论(0)
  • 2021-01-29 17:50

    If you can accommodate the domain of your values with the amount of memory that's available to your application, then, the fastest solution would be to represent your array as an array of bits:

    bool theArray[MAX_VALUE]; // of which 1024 values are true, the rest false
    uint32_t compareVal = 0x1234ABCD;
    bool validFlag = theArray[compareVal];
    

    EDIT

    I'm astounded by the number of critics. The title of this thread is "How do I quickly find whether a value is present in a C array?" for which I will stand by my answer because it answers precisely that. I could argue that this has the most speed efficient hash function (since address === value). I've read the comments and I'm aware of the obvious caveats. Undoubtedly those caveats limit the range of problems this can be used to solve, but, for those problems that it does solve, it solves very efficiently.

    Rather than reject this answer outright, consider it as the optimal starting point for which you can evolve by using hash functions to achieve a better balance between speed and performance.

    0 讨论(0)
  • 2021-01-29 17:50

    I'm sorry if my answer was already answered - just I'm a lazy reader. Feel you free to downvote then ))

    1) you could remove counter 'i' at all - just compare pointers, ie

    for (ptr = &the_array[0]; ptr < the_array+1024; ptr++)
    {
        if (compareVal == *ptr)
        {
           break;
        }
    }
    ... compare ptr and the_array+1024 here - you do not need validFlag at all.
    

    all that won't give any significant improvement though, such optimization probably could be achieved by the compiler itself.

    2) As it was already mentioned by other answers, almost all modern CPU are RISC-based, for example ARM. Even modern Intel X86 CPUs use RISC cores inside, as far as I know (compiling from X86 on fly). Major optimization for RISC is pipeline optimization (and for Intel and other CPU as well), minimizing code jumps. One type of such optimization (probably a major one), is "cycle rollback" one. It's incredibly stupid, and efficient, even Intel compiler can do that AFAIK. It looks like:

    if (compareVal == the_array[0]) { validFlag = true; goto end_of_compare; }
    if (compareVal == the_array[1]) { validFlag = true; goto end_of_compare; }
    ...and so on...
    end_of_compare:
    

    This way the optimization is that the pipeline is not broken for the worst case (if compareVal is absent in the array), so it is as fast as possible (of course not counting algorithm optimizations such as hash tables, sorted arrays and so on, mentioned in other answers, which may give better results depending on array size. Cycles Rollback approach can be applied there as well by the way. I'm writing here about that I think I didn't see in others)

    The second part of this optimization is that that array item is taken by direct address (calculated at compiling stage, make sure you use a static array), and do not need additional ADD op to calculate pointer from array's base address. This optimization may not have significant effect, since AFAIK ARM architecture has special features to speed up arrays addressing. But anyway it's always better to know that you did all the best just in C code directly, right?

    Cycle Rollback may look awkward due to waste of ROM (yep, you did right placing it to fast part of RAM, if your board supports this feature), but actually it's a fair pay for speed, being based on RISC concept. This is just a general point of calculation optimization - you sacrifice space for sake of speed, and vice versa, depending on your requirements.

    If you think that rollback for array of 1024 elements is too large sacrifice for your case, you can consider 'partial rollback', for example dividing the array into 2 parts of 512 items each, or 4x256, and so on.

    3) modern CPU often support SIMD ops, for example ARM NEON instruction set - it allows to execute the same ops in parallel. Frankly speaking I do not remember if it is suitable for comparison ops, but I feel it may be, you should check that. Googling shows that there may be some tricks as well, to get max speed, see https://stackoverflow.com/a/5734019/1028256

    I hope it can give you some new ideas.

    0 讨论(0)
  • 2021-01-29 17:57

    There's a trick for optimizing it (I was asked this on a job-interview once):

    • If the last entry in the array holds the value that you're looking for, then return true
    • Write the value that you're looking for into the last entry in the array
    • Iterate the array until you encounter the value that you're looking for
    • If you've encountered it before the last entry in the array, then return true
    • Return false

    bool check(uint32_t theArray[], uint32_t compareVal)
    {
        uint32_t i;
        uint32_t x = theArray[SIZE-1];
        if (x == compareVal)
            return true;
        theArray[SIZE-1] = compareVal;
        for (i = 0; theArray[i] != compareVal; i++);
        theArray[SIZE-1] = x;
        return i != SIZE-1;
    }
    

    This yields one branch per iteration instead of two branches per iteration.


    UPDATE:

    If you're allowed to allocate the array to SIZE+1, then you can get rid of the "last entry swapping" part:

    bool check(uint32_t theArray[], uint32_t compareVal)
    {
        uint32_t i;
        theArray[SIZE] = compareVal;
        for (i = 0; theArray[i] != compareVal; i++);
        return i != SIZE;
    }
    

    You can also get rid of the additional arithmetic embedded in theArray[i], using the following instead:

    bool check(uint32_t theArray[], uint32_t compareVal)
    {
        uint32_t *arrayPtr;
        theArray[SIZE] = compareVal;
        for (arrayPtr = theArray; *arrayPtr != compareVal; arrayPtr++);
        return arrayPtr != theArray+SIZE;
    }
    

    If the compiler doesn't already apply it, then this function will do so for sure. On the other hand, it might make it harder on the optimizer to unroll the loop, so you will have to verify that in the generated assembly code...

    0 讨论(0)
  • 2021-01-29 18:01

    Assuming your processor runs at 204 MHz which seems to be the maximum for the LPC4357, and also assuming your timing result reflects the average case (half of the array traversed), we get:

    • CPU frequency: 204 MHz
    • Cycle period: 4.9 ns
    • Duration in cycles: 12.5 µs / 4.9 ns = 2551 cycles
    • Cycles per iteration: 2551 / 128 = 19.9

    So, your search loop spends around 20 cycles per iteration. That doesn't sound awful, but I guess that in order to make it faster you need to look at the assembly.

    I would recommend dropping the index and using a pointer comparison instead, and making all the pointers const.

    bool arrayContains(const uint32_t *array, size_t length)
    {
      const uint32_t * const end = array + length;
      while(array != end)
      {
        if(*array++ == 0x1234ABCD)
          return true;
      }
      return false;
    }
    

    That's at least worth testing.

    0 讨论(0)
  • 2021-01-29 18:03

    This is more like an addendum than an answer.

    I've had a similar case in the past, but my array was constant over a considerable number of searches.

    In half of them, the searched value was NOT present in array. Then I realized I could apply a "filter" before doing any search.

    This "filter" is just a simple integer number, calculated ONCE and used in each search.

    It's in Java, but it's pretty simple:

    binaryfilter = 0;
    for (int i = 0; i < array.length; i++)
    {
        // just apply "Binary OR Operator" over values.
        binaryfilter = binaryfilter | array[i];
    }
    

    So, before do a binary search, I check binaryfilter:

    // Check binaryfilter vs value with a "Binary AND Operator"
    if ((binaryfilter & valuetosearch) != valuetosearch)
    {
        // valuetosearch is not in the array!
        return false;
    }
    else
    {
        // valuetosearch MAYBE in the array, so let's check it out
        // ... do binary search stuff ...
    
    }
    

    You can use a 'better' hash algorithm, but this can be very fast, specially for large numbers. May be this could save you even more cycles.

    0 讨论(0)
提交回复
热议问题