The previous objc_msgSend assembly analysis got the class and then started looking for the cache analysis

CacheLookup compiled and analyzed

     //CacheLookup LOOKUP, _objc_msgLookup, __objc_msgLookup_uncached
.macro CacheLookup Mode, Function, MissLabelDynamic, MissLabelConstant
	//
	// Restart protocol:
	//
	// As soon as we're past the LLookupStart\Function label we may have
	// loaded an invalid cache pointer or mask.
	//
	// When task_restartable_ranges_synchronize() is called,
	// (or when a signal hits us) before we're past LLookupEnd\Function,
	// then our PC will be reset to LLookupRecover\Function which forcefully
	// jumps to the cache-miss codepath which have the following
	// requirements:
	//
	// GETIMP:
	// The cache-miss is just returning NULL (setting x0 to 0)
	//
	// NORMAL and LOOKUP:
	// - x0 contains the receiver
	// - x1 contains the selector
	// - x16 contains the isa
	// - other registers are set as per calling conventions
	//
   // Move class to x15
	mov	x15, x16			// stash the original isa
LLookupStart\Function:
	// p1 = SEL, p16 = isa
//TARGET_OS_OSX/TARGET_OS_SIMULATOR
#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	ldr	p10, [x16, #CACHE]				// p10 = mask|buckets
	lsr	p11, p10, #48			// p11 = mask
	and	p10, p10, #0xffffffffffff	// p10 = buckets
	and	w12, w1, w11			// x12 = _cmd & mask
/ / machine arm64 really
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
//#define CACHE (2 * __SIZEOF_POINTER__)
//[x16, #CACHE] translates x16 (isa) CACHE size to cache_t
// LDR p11, cache_t Places cache_t on p11, that is, p11 = cache_t
	ldr	p11, [x16, #CACHE]			// p11 = mask|buckets
//CONFIG_USE_PREOPT_CACHES: arm64&&ios && not emulator && not MAC
#if CONFIG_USE_PREOPT_CACHES
//iphoneX and later devices
#if __has_feature(ptrauth_calls)
// Compare p11 with 0, if not, LLookupPreopt\Function
	tbnz	p11, #0, LLookupPreopt\Function
P11 (cache_t) starts at 0x0000FFFFFFFFFF and gives buckets to P10
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
#else
   P11 (cache_t) starts with buckets &0x0000FFfffffffe
	and	p10, p11, #0x0000fffffffffffe	// p10 = buckets
// Compare p11 with 0, if not, LLookupPreopt\Function
	tbnz	p11, #0, LLookupPreopt\Function
#endif
// p11 = 0
// P0 is receiver, p1 is sel
// eOR logic xor (^)
// Move P1 (sel) 7 positions to the right after ^ SEL, give it to p12
	eor	p12, p1, p1, LSR #7
// the first address of p11 (cache_t) is moved 48 bits to the right & p12 to p12
	and	p12, p12, p11, LSR #48		// x12 = (_cmd ^ (_cmd >> 7)) & mask
#else
	and	p10, p11, #0x0000ffffffffffff	// p10 = buckets
  // p11, LSR #48 p11 (cache_t
  //p1(sel) & mask get index to p12
	and	p12, p1, p11, LSR #48		// x12 = _cmd & mask
#endif // CONFIG_USE_PREOPT_CACHES
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	ldr	p11, [x16, #CACHE]				// p11 = mask|buckets
	and	p10, p11, #~0xf			// p10 = buckets
	and	p11, p11, #0xf			// p11 = maskShift
	mov	p12, #0xffff
	lsr	p11, p12, p11			// p11 = mask = 0xffff >> p11
	and	p12, p1, p11			// x12 = _cmd & mask
#else
#error Unsupported cache mask storage for ARM64.
#endif
// p11 cache -> p10 = buckets
// p1(_cmd) & mask = index -> p12
// #define PTRSHIFT 3
// P12 (index) moves to the left (PTRSHIFT+1)
//add p13, p10, p12: buckets + left (p13
	add	p13, p10, p12, LSL #(1+PTRSHIFT)
						// p13 = buckets + ((_cmd & mask) << (1+PTRSHIFT))

						// do {
// Put *bucket-- the contents in p17 and p9 respectively
Sel (p9) imp(p17) sel(p9)
1:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
If they are the same, they will go to 2. If they are not, they will go to 3f. In fact, it is a circular search
	cmp	p9, p1				// if (sel ! = _cmd) {
	b.ne	3f				// scan more
						// } else {
// Cache hit Mode is passed NORMAL
2:	CacheHit \Mode				// hit: call or return imp
						/ /}
/ / p9 is empty
3:	cbz	p9, \MissLabelDynamic		// if (sel == 0) goto Miss;
If p13 is greater than or equal to p10 (buckets) then bucket--
	cmp	p13, p10			// } while (bucket >= buckets)
	b.hs	1b
	// wrap-around:
	// p10 = first bucket
	// p11 = mask (and maybe other bits on LP64)
	// p12 = _cmd & mask
	//
	// A full cache can happen with CACHE_ALLOW_FULL_UTILIZATION.
	// So stop when we circle back to the first probed bucket
	// rather than when hitting the first bucket again.
	//
	// Note that we might probe the initial bucket twice
	// when the first probed slot is the last entry.


#if CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16_BIG_ADDRS
	add	p13, p10, w11, UXTW #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_HIGH_16
// Locate the last bucket
	add	p13, p10, p11, LSR #(48 - (1+PTRSHIFT))
						// p13 = buckets + (mask << 1+PTRSHIFT)
						// see comment about maskZeroBits
#elif CACHE_MASK_STORAGE == CACHE_MASK_STORAGE_LOW_4
	add	p13, p10, p11, LSL #(1+PTRSHIFT)
						// p13 = buckets + (mask << 1+PTRSHIFT)
#else
#error Unsupported cache mask storage for ARM64.
#endif
// The shared cache may also be stored for P12
	add	p12, p10, p12, LSL #(1+PTRSHIFT)
						// p12 = first probed bucket

						// do {
4:	ldp	p17, p9, [x13], #-BUCKET_SIZE	// {imp, sel} = *bucket--
	cmp	p9, p1				// if (sel == _cmd)
	b.eq	2b				// goto hit
	cmp	p9, #0				// } while (sel ! = 0 &&
	ccmp	p13, p12, #0, ne		// bucket > first_probed)
	b.hi	4b
Copy the code
// CacheHit: x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
.macro CacheHit
.if $0 == NORMAL
 如果 $0TailCallCachedImp x17, x10, x1, x16// authenticate and call imp
.elseif $0 == GETIMP
	mov	p0, p17
	cbz	p0, 9f			// don't ptrauth a nil imp
	AuthAndResignAsIMP x0, x10, x1, x16	// authenticate imp and re-sign as IMP
9:	ret				// return IMP
.elseif $0 == LOOKUP
	// No nil check for ptrauth: the caller would crash anyway when they
	// jump to a nil IMP. We don't care if that jump also fails ptrauth.
	AuthAndResignAsIMP x17, x10, x1, x16	// authenticate imp and re-sign as IMP
	cmp	x16, x15
	cinc	x16, x16, ne			// x16 += 1 when x15 ! = x16 (for instrumentation ; fallback to the parent class)
	ret				// return imp via x17
.else
.abort oops
.endif
Copy the code
.macro TailCallCachedImp
	// $0 = cached imp, $1 = address of cached imp, $2 = SEL, $3 = isa
   // x17 = cached IMP, x10 = address of buckets, x1 = SEL, x16 = isa
  // eOR logic xor (^)
  // $0^$3 = x17 ^ x16 = IMP ^ class IMP coding
	eor	$0, $0, $3
/ / call the IMP
	br	$0
Copy the code

The logic of CacheLookup looks something like this:

  1. willisaFirst address translation16In bytes, you getcache_tIn theobjc_classIn the structure,cacheThe front andisaandsuperclassEach have8Bytes.
  2. willcache_tThe first address of the0x0000fffffffffffegetbuckets
  3. willcache_twith0Compare, if not0Is executedLLookupPreopt\Function, if is0, the implementation of the first4step
  4. will_objc_msgSendThe second argument passed inSELAnd on themask(cacheMoves to the right48Who getmask)bucketThe subscriptindex
  5. bucketsThe first address+Offset (subscriptindexShift to the left (PTRSHIFT+ 1),PTRSHIFT=3) take outbucket
  6. do-whileCycle, whenbucketThe address is less thanbucketsThe loop ends at the first address. Loop the following operation:
  • 6.1 Decrease the address by BUCKET_SIZE each time

  • 6.2 Check whether the SEL in the bucket is the same as the SEL sent by objc_msgSend

  • 6.3 if they are the same, a CacheHit will occur. Execute CacheHit, IMP = IMP^ISA to obtain the IMP and invoke it

  • 6.4 If they are different, judge whether SEL extracted from bucket is empty. If it is empty, execute MissLabelDynamic; otherwise, decrease bucket address each time according to BUCKET_SIZE and search forward.

  • __objc_msgSend_uncached if not found at the end of the loop

Objc_msgSend and CacheLookup flowcharts

Objc_msgSend and CacheLookup flowchart summary

The objc_msgSend process is to get the IMP through sel and call imp. The process is as follows:

  1. judgerecevierIf there is a
  2. throughreceviertheisagetclass
  3. classMemory translation fetchcache, which containsbucketandmask
  4. cacheThe first address is up0x0000fffffffffffegetbuckets
  5. cacheMove the head address to the right48Who getmask. Need to bemaskbecauseinsertThe hash function is computed as follows:(mask_t)(value & mask)
  6. Get the first lookupindex.index = SEL&mask.
  7. bucket+indexNumber one in the entire cachebucket
  8. Take out thebucketIn thesel
  9. selwithobjc_msgSendThe incomingselCompare, equal hit cache, callIMP
  10. Unequal let it bebucket--To return to the first7Step start cycle
  11. Execute if the end of the loop is not found__objc_msgSend_uncached

supplement

This is the first red boxvalueis0When a method is called[p saySomething](Second red box),valueis7According to the previous pairCache_ttheexplore, the initial capacity is4, when the capacity is greater than the current volumeThree quarters of, will be expanded. So when you callsaySomethingWhen, what other methods occupy the capacity?

In cache_t:: INSERT method unsigned oldCapacity = capacity(), capacity = oldCapacity; Insert the following code after:

if (sel == @selector(saySomething)) {
        bucket_t *kc_b = buckets(a);for (unsigned i = 0; i<oldCapacity; i++) {
            SEL kc_sel = kc_b[i].sel(a); IMP kc_imp = kc_b[i].imp(kc_b,nil);
            printf("%p - %p - %p\n",kc_sel,kc_imp,&kc_b[i]);
        }
        printf("isConstantEmptyCahe %p - %u - %u -%u\n",kc_b,capacity,newOccupied,oldCapacity);
    }
Copy the code

The purpose of inserting this code is to callsaySomethingMethod, prints out the current capacity using method.lldbThe debugging result is as follows:The first two arerespondsToSelector:andclassThe third one is empty. The fourthimpAddress and the first onebucketIt’s the same address both of them0x10144d050. inallocateBucketsFunction,That means an extra one will be added at the end of the listbucket.selfor1And point to the first onebucket.

So back to the question, why is value 7?

Because there are currently four methods, which are larger than 3/4 of the current capacity (default 4), the capacity has been expanded.