ConcurrentPtrHashSet.h   [plain text]


/*
 * Copyright (C) 2017 Apple Inc. All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 * 1. Redistributions of source code must retain the above copyright
 *    notice, this list of conditions and the following disclaimer.
 * 2. Redistributions in binary form must reproduce the above copyright
 *    notice, this list of conditions and the following disclaimer in the
 *    documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY APPLE INC. ``AS IS'' AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
 * PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE INC. OR
 * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
 * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
 * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
 * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
 * OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 
 */

#pragma once

#include <wtf/Atomics.h>
#include <wtf/FastMalloc.h>
#include <wtf/HashFunctions.h>
#include <wtf/Lock.h>
#include <wtf/Noncopyable.h>
#include <wtf/Vector.h>

namespace WTF {

// This is a concurrent hash-based set for pointers. It's optimized for:
//
// - High rate of contains() calls.
// - High rate of add() calls that don't add anything new. add() calls that don't add anything (nop adds)
//   don't mutate the table at all.
// - Not too many threads. I doubt this scales beyond ~4. Though, it may actually scale better than that
//   if the rate of nop adds is absurdly high.
//
// If we wanted this to scale better, the main change we'd have to make is how this table determines when
// to resize. Right now it's a shared counter. We lock;xadd this counter. One easy way to make that
// scalable is to require each thread that works with the ConcurrentPtrHashSet to register itself first.
// Then each thread would have some data structure that has a counter. We could institute the policy that
// each thread simply increments its own load counter, in its own data structure. Then, if any search to
// resolve a collision does more than N iterations, we can compute a combined load by summing the load
// counters of all of the thread data structures.
//
// ConcurrentPtrHashSet's main user, the GC, sees a 98% nop add rate in Speedometer. That's why this
// focuses so much on cases where the table does not change.
class ConcurrentPtrHashSet {
    WTF_MAKE_NONCOPYABLE(ConcurrentPtrHashSet);
    WTF_MAKE_FAST_ALLOCATED;

public:
    WTF_EXPORT_PRIVATE ConcurrentPtrHashSet();
    WTF_EXPORT_PRIVATE ~ConcurrentPtrHashSet();
    
    template<typename T>
    bool contains(T value)
    {
        return containsImpl(cast(value));
    }
    
    template<typename T>
    bool add(T value)
    {
        return addImpl(cast(value));
    }
    
    size_t size() const
    {
        return m_table.loadRelaxed()->load.loadRelaxed();
    }
    
    // Only call when you know that no other thread can call add(). This frees up memory without changing
    // the contents of the table.
    WTF_EXPORT_PRIVATE void deleteOldTables();
    
    // Only call when you know that no other thread can call add(). This frees up all memory except for
    // the smallest possible hashtable.
    WTF_EXPORT_PRIVATE void clear();
    
private:
    struct Table {
        WTF_MAKE_STRUCT_FAST_ALLOCATED;
        
        static std::unique_ptr<Table> create(unsigned size);
        
        unsigned maxLoad() const { return size / 2; }
        
        unsigned size; // This is immutable.
        unsigned mask; // This is immutable.
        Atomic<unsigned> load;
        Atomic<void*> array[1];
    };
    
    static unsigned hash(void* ptr)
    {
        return PtrHash<void*>::hash(ptr);
    }
    
    void initialize();
    
    template<typename T>
    void* cast(T value)
    {
        static_assert(sizeof(T) <= sizeof(void*), "type too big");
        union {
            void* ptr;
            T value;
        } u;
        u.ptr = nullptr;
        u.value = value;
        return u.ptr;
    }
    
    bool containsImpl(void* ptr) const
    {
        Table* table = m_table.loadRelaxed();
        unsigned mask = table->mask;
        unsigned startIndex = hash(ptr) & mask;
        unsigned index = startIndex;
        for (;;) {
            void* entry = table->array[index].loadRelaxed();
            if (!entry)
                return false;
            if (entry == ptr)
                return true;
            index = (index + 1) & mask;
            RELEASE_ASSERT(index != startIndex);
        }
    }
    
    // Returns true if a new entry was added.
    bool addImpl(void* ptr)
    {
        Table* table = m_table.loadRelaxed();
        unsigned mask = table->mask;
        unsigned startIndex = hash(ptr) & mask;
        unsigned index = startIndex;
        for (;;) {
            void* entry = table->array[index].loadRelaxed();
            if (!entry)
                return addSlow(table, mask, startIndex, index, ptr);
            if (entry == ptr)
                return false;
            index = (index + 1) & mask;
            RELEASE_ASSERT(index != startIndex);
        }
    }
    
    WTF_EXPORT_PRIVATE bool addSlow(Table* table, unsigned mask, unsigned startIndex, unsigned index, void* ptr);

    void resizeIfNecessary();
    bool resizeAndAdd(void* ptr);
    
    Vector<std::unique_ptr<Table>, 4> m_allTables;
    Atomic<Table*> m_table; // This is never null.
    Lock m_lock; // We just use this to control resize races.
};

} // namespace WTF

using WTF::ConcurrentPtrHashSet;