Weak(2) - - weak_entry_t of OC Runtime

The last article mainly introduced the basic concept of weak reference, and the basic structure and implementation of weak_table_t. Weak_entry_t is the specific data type stored in weak_table_t. This paper continues to dig deeply to explore the data structure and implementation of weak_entry_t.

struct weak_entry_t {
    DisguisedPtr<objc_object> referent;
    union {
        struct {
            weak_referrer_t *referrers;
            uintptr_t        out_of_line_ness : 2;
            uintptr_t        num_refs : PTR_MINUS_2;
            uintptr_t        mask;
            uintptr_t        max_hash_displacement;
        };
        struct {
            // out_of_line_ness field is low bits of inline_referrers[1]
            weak_referrer_t  inline_referrers[WEAK_INLINE_COUNT];
        };
    };

    bool out_of_line() {
        return (out_of_line_ness == REFERRERS_OUT_OF_LINE);
    }

    weak_entry_t& operator=(const weak_entry_t& other) {
        memcpy(this, &other, sizeof(other));
        return *this;
    }

    weak_entry_t(objc_object *newReferent, objc_object **newReferrer)
        : referent(newReferent)
    {
        inline_referrers[0] = newReferrer;
        for (int i = 1; i < WEAK_INLINE_COUNT; i++) {
            inline_referrers[i] = nil;
        }
    }
};

DisguisedPtr is a runtime encapsulation of ordinary object pointers (references) to hide the internal pointer of weak_table_t. Its definition is as follows:

typedef DisguisedPtr<objc_object *> weak_referrer_t;

As you can see, the structure of weak_entry_t is a little similar to that of weak_table_t, which is basically an implementation of HashTable. num_refs, mask and max_hash_displacement have all appeared in weak_table_t, and their functions are basically the same, so they are not described in detail.

Weak_entry_t has a clever design, that is, if the number of weak references corresponding to an object is small (<=WEAK_INLINE_COUNT, runtime sets this value to 4), then its weak references are stored in an inline array in turn. The memory of this inline array will be allocated together when weak_entry_t is initialized, instead of applying for new memory space when needed, so as to achieve the purpose of efficiency. In addition, two struct s in union share the same block of memory. If you do not use inline arrays and use HashTable directly, then num_refs, mask and max_hash_displacement variables need separate storage space, which will use more memory. In summary, the use of inline arrays not only saves a certain amount of memory space, but also relatively improves the efficiency of operation.

Out_of_line_ness occupies the lowest two-digit space of inline_referrers[1]. It is also the tag bit of weak_entry_t that currently uses inline arrays or out line arrays (i.e. HashTable implementations). Since the minimum two bit guarantees for DisguisedPtr are 00 or 11 (the implementation of DisguisedPtr is not listed here, which does not affect the understanding of weak_entry_t), if out_of_line_ness is not zero, it means that weak_entry_t no longer uses inline arrays. This point can be confirmed by the implementation of out_of_line function. It needs to be judged that the value of out_of_line_ness is equal to REFERRERS_OUT_OF_LINE. REFERRERS_OUT_OF_LINE is set to 2 in runtime and can be represented by two bits.

The main functions for weak_entry_t are growth_refs_and_insert and append_referrer. The weak_table_t mentioned in the previous article has a weak_register_no_lock function, in which the append_referrer function is called to save new weak references for an object.

static void append_referrer(weak_entry_t *entry, objc_object **new_referrer)
{
    if (! entry->out_of_line()) {
        // Try to insert inline.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == nil) {
                entry->inline_referrers[i] = new_referrer;
                return;
            }
        }

        // Couldn't insert inline. Allocate out of line.
        weak_referrer_t *new_referrers = (weak_referrer_t *)
            calloc(WEAK_INLINE_COUNT, sizeof(weak_referrer_t));
        // This constructed table is invalid, but grow_refs_and_insert
        // will fix it and rehash it.
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            new_referrers[i] = entry->inline_referrers[i];
        }
        entry->referrers = new_referrers;
        entry->num_refs = WEAK_INLINE_COUNT;
        entry->out_of_line_ness = REFERRERS_OUT_OF_LINE;
        entry->mask = WEAK_INLINE_COUNT-1;
        entry->max_hash_displacement = 0;
    }

    assert(entry->out_of_line());

    if (entry->num_refs >= TABLE_SIZE(entry) * 3/4) {
        return grow_refs_and_insert(entry, new_referrer);
    }
    size_t begin = w_hash_pointer(new_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != nil) {
        hash_displacement++;
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
    }
    if (hash_displacement > entry->max_hash_displacement) {
        entry->max_hash_displacement = hash_displacement;
    }
    weak_referrer_t &ref = entry->referrers[index];
    ref = new_referrer;
    entry->num_refs++;
}
  • The append_referrer function first handles the case where weak_entry_t is still using an inline array. First, try inserting a new weak reference into the inline array. If the inline array is full, create a new array of WEAK_INLINE_COUNT size. Copy the elements in the inline array in turn by outline.
  • The latter half of the function deals with the use of outline arrays. If the utilization rate of outline arrays is 75% or more, then call the grow_refs_and_insert function to expand and insert new weak references. Otherwise, the hash insertion will be performed directly, and the insertion process is basically the same as that of weak_table_t.
__attribute__((noinline, used))
static void grow_refs_and_insert(weak_entry_t *entry, 
                                 objc_object **new_referrer)
{
    assert(entry->out_of_line());

    size_t old_size = TABLE_SIZE(entry);
    size_t new_size = old_size ? old_size * 2 : 8;

    size_t num_refs = entry->num_refs;
    weak_referrer_t *old_refs = entry->referrers;
    entry->mask = new_size - 1;
    
    entry->referrers = (weak_referrer_t *)
        calloc(TABLE_SIZE(entry), sizeof(weak_referrer_t));
    entry->num_refs = 0;
    entry->max_hash_displacement = 0;
    
    for (size_t i = 0; i < old_size && num_refs > 0; i++) {
        if (old_refs[i] != nil) {
            append_referrer(entry, old_refs[i]);
            num_refs--;
        }
    }
    // Insert
    append_referrer(entry, new_referrer);
    if (old_refs) free(old_refs);
}
  • The function first expands the outline array, which has twice the original capacity. Then the element hash in the old array is inserted into the new array in turn, and finally hash is inserted into the new reference.
static void remove_referrer(weak_entry_t *entry, objc_object **old_referrer)
{
    if (! entry->out_of_line()) {
        for (size_t i = 0; i < WEAK_INLINE_COUNT; i++) {
            if (entry->inline_referrers[i] == old_referrer) {
                entry->inline_referrers[i] = nil;
                return;
            }
        }
        _objc_inform("Attempted to unregister unknown __weak variable "
                     "at %p. This is probably incorrect use of "
                     "objc_storeWeak() and objc_loadWeak(). "
                     "Break on objc_weak_error to debug.\n", 
                     old_referrer);
        objc_weak_error();
        return;
    }

    size_t begin = w_hash_pointer(old_referrer) & (entry->mask);
    size_t index = begin;
    size_t hash_displacement = 0;
    while (entry->referrers[index] != old_referrer) {
        index = (index+1) & entry->mask;
        if (index == begin) bad_weak_table(entry);
        hash_displacement++;
        if (hash_displacement > entry->max_hash_displacement) {
            _objc_inform("Attempted to unregister unknown __weak variable "
                         "at %p. This is probably incorrect use of "
                         "objc_storeWeak() and objc_loadWeak(). "
                         "Break on objc_weak_error to debug.\n", 
                         old_referrer);
            objc_weak_error();
            return;
        }
    }
    entry->referrers[index] = nil;
    entry->num_refs--;
}
  • The remove_referrer function is responsible for deleting a weak reference. The function first deals with the inline array and directly empties the corresponding weak reference term. If an outline array is used, the item to be deleted is found by hash and deleted directly. The operation corresponding to weak_table_t is basically the same.

So far, the data structure and operation of weak_entry_t have been introduced. It can be seen that Apple still spends a lot of time on program efficiency and memory optimization. Subsequently, we will introduce the parts associated with weak references in NSObject implementations, which together constitute the entire weak reference mechanism.

Posted by phpnewbiy on Fri, 11 Jan 2019 15:06:12 -0800