| /* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 4 -*- |
| * vim: set ts=8 sts=4 et sw=4 tw=99: |
| * This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
| |
| #ifndef jit_shared_IonAssemblerBufferWithConstantPools_h |
| #define jit_shared_IonAssemblerBufferWithConstantPools_h |
| |
| #include "mozilla/DebugOnly.h" |
| |
| #include <algorithm> |
| |
| #include "jit/JitSpewer.h" |
| #include "jit/shared/IonAssemblerBuffer.h" |
| |
| // This code extends the AssemblerBuffer to support the pooling of values loaded |
| // using program-counter relative addressing modes. This is necessary with the |
| // ARM instruction set because it has a fixed instruction size that can not |
| // encode all values as immediate arguments in instructions. Pooling the values |
| // allows the values to be placed in large chunks which minimizes the number of |
| // forced branches around them in the code. This is used for loading floating |
| // point constants, for loading 32 bit constants on the ARMv6, for absolute |
| // branch targets, and in future will be needed for large branches on the ARMv6. |
| // |
| // For simplicity of the implementation, the constant pools are always placed |
| // after the loads referencing them. When a new constant pool load is added to |
| // the assembler buffer, a corresponding pool entry is added to the current |
| // pending pool. The finishPool() method copies the current pending pool entries |
| // into the assembler buffer at the current offset and patches the pending |
| // constant pool load instructions. |
| // |
| // Before inserting instructions or pool entries, it is necessary to determine |
| // if doing so would place a pending pool entry out of reach of an instruction, |
| // and if so then the pool must firstly be dumped. With the allocation algorithm |
| // used below, the recalculation of all the distances between instructions and |
| // their pool entries can be avoided by noting that there will be a limiting |
| // instruction and pool entry pair that does not change when inserting more |
| // instructions. Adding more instructions makes the same increase to the |
| // distance, between instructions and their pool entries, for all such |
| // pairs. This pair is recorded as the limiter, and it is updated when new pool |
| // entries are added, see updateLimiter() |
| // |
| // The pools consist of: a guard instruction that branches around the pool, a |
| // header word that helps identify a pool in the instruction stream, and then |
| // the pool entries allocated in units of words. The guard instruction could be |
| // omitted if control does not reach the pool, and this is referred to as a |
| // natural guard below, but for simplicity the guard branch is always |
| // emitted. The pool header is an identifiable word that in combination with the |
| // guard uniquely identifies a pool in the instruction stream. The header also |
| // encodes the pool size and a flag indicating if the guard is natural. It is |
| // possible to iterate through the code instructions skipping or examining the |
| // pools. E.g. it might be necessary to skip pools when search for, or patching, |
| // an instruction sequence. |
| // |
| // It is often required to keep a reference to a pool entry, to patch it after |
| // the buffer is finished. Each pool entry is assigned a unique index, counting |
| // up from zero (see the poolEntryCount slot below). These can be mapped back to |
| // the offset of the pool entry in the finished buffer, see poolEntryOffset(). |
| // |
| // The code supports no-pool regions, and for these the size of the region, in |
| // instructions, must be supplied. This size is used to determine if inserting |
| // the instructions would place a pool entry out of range, and if so then a pool |
| // is firstly flushed. The DEBUG code checks that the emitted code is within the |
| // supplied size to detect programming errors. See enterNoPool() and |
| // leaveNoPool(). |
| |
| // The only planned instruction sets that require inline constant pools are the |
| // ARM, ARM64, and MIPS, and these all have fixed 32-bit sized instructions so |
| // for simplicity the code below is specialized for fixed 32-bit sized |
| // instructions and makes no attempt to support variable length |
| // instructions. The base assembler buffer which supports variable width |
| // instruction is used by the x86 and x64 backends. |
| |
| // The AssemblerBufferWithConstantPools template class uses static callbacks to |
| // the provided Asm template argument class: |
| // |
| // void Asm::InsertIndexIntoTag(uint8_t* load_, uint32_t index) |
| // |
| // When allocEntry() is called to add a constant pool load with an associated |
| // constant pool entry, this callback is called to encode the index of the |
| // allocated constant pool entry into the load instruction. |
| // |
| // After the constant pool has been placed, PatchConstantPoolLoad() is called |
| // to update the load instruction with the right load offset. |
| // |
| // void Asm::WritePoolGuard(BufferOffset branch, |
| // Instruction* dest, |
| // BufferOffset afterPool) |
| // |
| // Write out the constant pool guard branch before emitting the pool. |
| // |
| // branch |
| // Offset of the guard branch in the buffer. |
| // |
| // dest |
| // Pointer into the buffer where the guard branch should be emitted. (Same |
| // as getInst(branch)). Space for guardSize_ instructions has been reserved. |
| // |
| // afterPool |
| // Offset of the first instruction after the constant pool. This includes |
| // both pool entries and branch veneers added after the pool data. |
| // |
| // void Asm::WritePoolHeader(uint8_t* start, Pool* p, bool isNatural) |
| // |
| // Write out the pool header which follows the guard branch. |
| // |
| // void Asm::PatchConstantPoolLoad(void* loadAddr, void* constPoolAddr) |
| // |
| // Re-encode a load of a constant pool entry after the location of the |
| // constant pool is known. |
| // |
| // The load instruction at loadAddr was previously passed to |
| // InsertIndexIntoTag(). The constPoolAddr is the final address of the |
| // constant pool in the assembler buffer. |
| // |
| // void Asm::PatchShortRangeBranchToVeneer(AssemblerBufferWithConstantPools*, |
| // unsigned rangeIdx, |
| // BufferOffset deadline, |
| // BufferOffset veneer) |
| // |
| // Patch a short-range branch to jump through a veneer before it goes out of |
| // range. |
| // |
| // rangeIdx, deadline |
| // These arguments were previously passed to registerBranchDeadline(). It is |
| // assumed that PatchShortRangeBranchToVeneer() knows how to compute the |
| // offset of the short-range branch from this information. |
| // |
| // veneer |
| // Space for a branch veneer, guaranteed to be <= deadline. At this |
| // position, guardSize_ * InstSize bytes are allocated. They should be |
| // initialized to the proper unconditional branch instruction. |
| // |
| // Unbound branches to the same unbound label are organized as a linked list: |
| // |
| // Label::offset -> Branch1 -> Branch2 -> Branch3 -> nil |
| // |
| // This callback should insert a new veneer branch into the list: |
| // |
| // Label::offset -> Branch1 -> Branch2 -> Veneer -> Branch3 -> nil |
| // |
| // When Assembler::bind() rewrites the branches with the real label offset, it |
| // probably has to bind Branch2 to target the veneer branch instead of jumping |
| // straight to the label. |
| |
| namespace js { |
| namespace jit { |
| |
| // BranchDeadlineSet - Keep track of pending branch deadlines. |
| // |
| // Some architectures like arm and arm64 have branch instructions with limited |
| // range. When assembling a forward branch, it is not always known if the final |
| // target label will be in range of the branch instruction. |
| // |
| // The BranchDeadlineSet data structure is used to keep track of the set of |
| // pending forward branches. It supports the following fast operations: |
| // |
| // 1. Get the earliest deadline in the set. |
| // 2. Add a new branch deadline. |
| // 3. Remove a branch deadline. |
| // |
| // Architectures may have different branch encodings with different ranges. Each |
| // supported range is assigned a small integer starting at 0. This data |
| // structure does not care about the actual range of branch instructions, just |
| // the latest buffer offset that can be reached - the deadline offset. |
| // |
| // Branched are stored as (rangeIdx, deadline) tuples. The target-specific code |
| // can compute the location of the branch itself from this information. This |
| // data structure does not need to know. |
| // |
| template <unsigned NumRanges> |
| class BranchDeadlineSet |
| { |
| // Maintain a list of pending deadlines for each range separately. |
| // |
| // The offsets in each vector are always kept in ascending order. |
| // |
| // Because we have a separate vector for different ranges, as forward |
| // branches are added to the assembler buffer, their deadlines will |
| // always be appended to the vector corresponding to their range. |
| // |
| // When binding labels, we expect a more-or-less LIFO order of branch |
| // resolutions. This would always hold if we had strictly structured control |
| // flow. |
| // |
| // We allow branch deadlines to be added and removed in any order, but |
| // performance is best in the expected case of near LIFO order. |
| // |
| typedef Vector<BufferOffset, 8, LifoAllocPolicy<Fallible>> RangeVector; |
| |
| // We really just want "RangeVector deadline_[NumRanges];", but each vector |
| // needs to be initialized with a LifoAlloc, and C++ doesn't bend that way. |
| // |
| // Use raw aligned storage instead and explicitly construct NumRanges |
| // vectors in our constructor. |
| mozilla::AlignedStorage2<RangeVector[NumRanges]> deadlineStorage_; |
| |
| // Always access the range vectors through this method. |
| RangeVector& vectorForRange(unsigned rangeIdx) |
| { |
| MOZ_ASSERT(rangeIdx < NumRanges, "Invalid branch range index"); |
| return (*deadlineStorage_.addr())[rangeIdx]; |
| } |
| |
| const RangeVector& vectorForRange(unsigned rangeIdx) const |
| { |
| MOZ_ASSERT(rangeIdx < NumRanges, "Invalid branch range index"); |
| return (*deadlineStorage_.addr())[rangeIdx]; |
| } |
| |
| // Maintain a precomputed earliest deadline at all times. |
| // This is unassigned only when all deadline vectors are empty. |
| BufferOffset earliest_; |
| |
| // The range vector owning earliest_. Uninitialized when empty. |
| unsigned earliestRange_; |
| |
| // Recompute the earliest deadline after it's been invalidated. |
| void recomputeEarliest() |
| { |
| earliest_ = BufferOffset(); |
| for (unsigned r = 0; r < NumRanges; r++) { |
| auto& vec = vectorForRange(r); |
| if (!vec.empty() && (!earliest_.assigned() || vec[0] < earliest_)) { |
| earliest_ = vec[0]; |
| earliestRange_ = r; |
| } |
| } |
| } |
| |
| // Update the earliest deadline if needed after inserting (rangeIdx, |
| // deadline). Always return true for convenience: |
| // return insert() && updateEarliest(). |
| bool updateEarliest(unsigned rangeIdx, BufferOffset deadline) |
| { |
| if (!earliest_.assigned() || deadline < earliest_) { |
| earliest_ = deadline; |
| earliestRange_ = rangeIdx; |
| } |
| return true; |
| } |
| |
| public: |
| explicit BranchDeadlineSet(LifoAlloc& alloc) |
| { |
| // Manually construct vectors in the uninitialized aligned storage. |
| // This is because C++ arrays can otherwise only be constructed with |
| // the default constructor. |
| for (unsigned r = 0; r < NumRanges; r++) |
| new (&vectorForRange(r)) RangeVector(alloc); |
| } |
| |
| ~BranchDeadlineSet() |
| { |
| // Aligned storage doesn't destruct its contents automatically. |
| for (unsigned r = 0; r < NumRanges; r++) |
| vectorForRange(r).~RangeVector(); |
| } |
| |
| // Is this set completely empty? |
| bool empty() const { return !earliest_.assigned(); } |
| |
| // Get the total number of deadlines in the set. |
| size_t size() const |
| { |
| size_t count = 0; |
| for (unsigned r = 0; r < NumRanges; r++) |
| count += vectorForRange(r).length(); |
| return count; |
| } |
| |
| // Get the number of deadlines for the range with the most elements. |
| size_t maxRangeSize() const |
| { |
| size_t count = 0; |
| for (unsigned r = 0; r < NumRanges; r++) |
| count = std::max(count, vectorForRange(r).length()); |
| return count; |
| } |
| |
| // Get the first deadline that is still in the set. |
| BufferOffset earliestDeadline() const |
| { |
| MOZ_ASSERT(!empty()); |
| return earliest_; |
| } |
| |
| // Get the range index corresponding to earliestDeadlineRange(). |
| unsigned earliestDeadlineRange() const |
| { |
| MOZ_ASSERT(!empty()); |
| return earliestRange_; |
| } |
| |
| // Add a (rangeIdx, deadline) tuple to the set. |
| // |
| // It is assumed that this tuple is not already in the set. |
| // This function performs best id the added deadline is later than any |
| // existing deadline for the same range index. |
| // |
| // Return true if the tuple was added, false if the tuple could not be added |
| // because of an OOM error. |
| bool addDeadline(unsigned rangeIdx, BufferOffset deadline) |
| { |
| MOZ_ASSERT(deadline.assigned(), "Can only store assigned buffer offsets"); |
| // This is the vector where deadline should be saved. |
| auto& vec = vectorForRange(rangeIdx); |
| |
| // Fast case: Simple append to the relevant array. This never affects |
| // the earliest deadline. |
| if (!vec.empty() && vec.back() < deadline) |
| return vec.append(deadline); |
| |
| // Fast case: First entry to the vector. We need to update earliest_. |
| if (vec.empty()) |
| return vec.append(deadline) && updateEarliest(rangeIdx, deadline); |
| |
| return addDeadlineSlow(rangeIdx, deadline); |
| } |
| |
| private: |
| // General case of addDeadline. This is split into two functions such that |
| // the common case in addDeadline can be inlined while this part probably |
| // won't inline. |
| bool addDeadlineSlow(unsigned rangeIdx, BufferOffset deadline) |
| { |
| auto& vec = vectorForRange(rangeIdx); |
| |
| // Inserting into the middle of the vector. Use a log time binary search |
| // and a linear time insert(). |
| // Is it worthwhile special-casing the empty vector? |
| auto at = std::lower_bound(vec.begin(), vec.end(), deadline); |
| MOZ_ASSERT(at == vec.end() || *at != deadline, "Cannot insert duplicate deadlines"); |
| return vec.insert(at, deadline) && updateEarliest(rangeIdx, deadline); |
| } |
| |
| public: |
| // Remove a deadline from the set. |
| // If (rangeIdx, deadline) is not in the set, nothing happens. |
| void removeDeadline(unsigned rangeIdx, BufferOffset deadline) |
| { |
| auto& vec = vectorForRange(rangeIdx); |
| |
| if (vec.empty()) |
| return; |
| |
| if (deadline == vec.back()) { |
| // Expected fast case: Structured control flow causes forward |
| // branches to be bound in reverse order. |
| vec.popBack(); |
| } else { |
| // Slow case: Binary search + linear erase. |
| auto where = std::lower_bound(vec.begin(), vec.end(), deadline); |
| if (where == vec.end() || *where != deadline) |
| return; |
| vec.erase(where); |
| } |
| if (deadline == earliest_) |
| recomputeEarliest(); |
| } |
| }; |
| |
| // Specialization for architectures that don't need to track short-range |
| // branches. |
| template <> |
| class BranchDeadlineSet<0u> |
| { |
| public: |
| explicit BranchDeadlineSet(LifoAlloc& alloc) {} |
| bool empty() const { return true; } |
| size_t size() const { return 0; } |
| size_t maxRangeSize() const { return 0; } |
| BufferOffset earliestDeadline() const { MOZ_CRASH(); } |
| unsigned earliestDeadlineRange() const { MOZ_CRASH(); } |
| bool addDeadline(unsigned rangeIdx, BufferOffset deadline) { MOZ_CRASH(); } |
| void removeDeadline(unsigned rangeIdx, BufferOffset deadline) { MOZ_CRASH(); } |
| }; |
| |
| // The allocation unit size for pools. |
| typedef int32_t PoolAllocUnit; |
| |
| // Hysteresis given to short-range branches. |
| // |
| // If any short-range branches will go out of range in the next N bytes, |
| // generate a veneer for them in the current pool. The hysteresis prevents the |
| // creation of many tiny constant pools for branch veneers. |
| const size_t ShortRangeBranchHysteresis = 128; |
| |
| struct Pool |
| { |
| private: |
| // The maximum program-counter relative offset below which the instruction |
| // set can encode. Different classes of intructions might support different |
| // ranges but for simplicity the minimum is used here, and for the ARM this |
| // is constrained to 1024 by the float load instructions. |
| const size_t maxOffset_; |
| // An offset to apply to program-counter relative offsets. The ARM has a |
| // bias of 8. |
| const unsigned bias_; |
| |
| // The content of the pool entries. |
| Vector<PoolAllocUnit, 8, LifoAllocPolicy<Fallible>> poolData_; |
| |
| // Flag that tracks OOM conditions. This is set after any append failed. |
| bool oom_; |
| |
| // The limiting instruction and pool-entry pair. The instruction program |
| // counter relative offset of this limiting instruction will go out of range |
| // first as the pool position moves forward. It is more efficient to track |
| // just this limiting pair than to recheck all offsets when testing if the |
| // pool needs to be dumped. |
| // |
| // 1. The actual offset of the limiting instruction referencing the limiting |
| // pool entry. |
| BufferOffset limitingUser; |
| // 2. The pool entry index of the limiting pool entry. |
| unsigned limitingUsee; |
| |
| public: |
| // A record of the code offset of instructions that reference pool |
| // entries. These instructions need to be patched when the actual position |
| // of the instructions and pools are known, and for the code below this |
| // occurs when each pool is finished, see finishPool(). |
| Vector<BufferOffset, 8, LifoAllocPolicy<Fallible>> loadOffsets; |
| |
| // Create a Pool. Don't allocate anything from lifoAloc, just capture its reference. |
| explicit Pool(size_t maxOffset, unsigned bias, LifoAlloc& lifoAlloc) |
| : maxOffset_(maxOffset), |
| bias_(bias), |
| poolData_(lifoAlloc), |
| oom_(false), |
| limitingUser(), |
| limitingUsee(INT_MIN), |
| loadOffsets(lifoAlloc) |
| { |
| } |
| |
| // If poolData() returns nullptr then oom_ will also be true. |
| const PoolAllocUnit* poolData() const { |
| return poolData_.begin(); |
| } |
| |
| unsigned numEntries() const { |
| return poolData_.length(); |
| } |
| |
| size_t getPoolSize() const { |
| return numEntries() * sizeof(PoolAllocUnit); |
| } |
| |
| bool oom() const { |
| return oom_; |
| } |
| |
| // Update the instruction/pool-entry pair that limits the position of the |
| // pool. The nextInst is the actual offset of the new instruction being |
| // allocated. |
| // |
| // This is comparing the offsets, see checkFull() below for the equation, |
| // but common expressions on both sides have been canceled from the ranges |
| // being compared. Notably, the poolOffset cancels out, so the limiting pair |
| // does not depend on where the pool is placed. |
| void updateLimiter(BufferOffset nextInst) { |
| ptrdiff_t oldRange = limitingUsee * sizeof(PoolAllocUnit) - limitingUser.getOffset(); |
| ptrdiff_t newRange = getPoolSize() - nextInst.getOffset(); |
| if (!limitingUser.assigned() || newRange > oldRange) { |
| // We have a new largest range! |
| limitingUser = nextInst; |
| limitingUsee = numEntries(); |
| } |
| } |
| |
| // Check if inserting a pool at the actual offset poolOffset would place |
| // pool entries out of reach. This is called before inserting instructions |
| // to check that doing so would not push pool entries out of reach, and if |
| // so then the pool would need to be firstly dumped. The poolOffset is the |
| // first word of the pool, after the guard and header and alignment fill. |
| bool checkFull(size_t poolOffset) const { |
| // Not full if there are no uses. |
| if (!limitingUser.assigned()) |
| return false; |
| size_t offset = poolOffset + limitingUsee * sizeof(PoolAllocUnit) |
| - (limitingUser.getOffset() + bias_); |
| return offset >= maxOffset_; |
| } |
| |
| static const unsigned OOM_FAIL = unsigned(-1); |
| |
| unsigned insertEntry(unsigned num, uint8_t* data, BufferOffset off, LifoAlloc& lifoAlloc) { |
| if (oom_) |
| return OOM_FAIL; |
| unsigned ret = numEntries(); |
| if (!poolData_.append((PoolAllocUnit*)data, num) || !loadOffsets.append(off)) { |
| oom_ = true; |
| return OOM_FAIL; |
| } |
| return ret; |
| } |
| |
| void reset() { |
| poolData_.clear(); |
| loadOffsets.clear(); |
| |
| limitingUser = BufferOffset(); |
| limitingUsee = -1; |
| } |
| }; |
| |
| |
| // Template arguments: |
| // |
| // SliceSize |
| // Number of bytes in each allocated BufferSlice. See |
| // AssemblerBuffer::SliceSize. |
| // |
| // InstSize |
| // Size in bytes of the fixed-size instructions. This should be equal to |
| // sizeof(Inst). This is only needed here because the buffer is defined before |
| // the Instruction. |
| // |
| // Inst |
| // The actual type used to represent instructions. This is only really used as |
| // the return type of the getInst() method. |
| // |
| // Asm |
| // Class defining the needed static callback functions. See documentation of |
| // the Asm::* callbacks above. |
| // |
| // NumShortBranchRanges |
| // The number of short branch ranges to support. This can be 0 if no support |
| // for tracking short range branches is needed. The |
| // AssemblerBufferWithConstantPools class does not need to know what the range |
| // of branches is - it deals in branch 'deadlines' which is the last buffer |
| // position that a short-range forward branch can reach. It is assumed that |
| // the Asm class is able to find the actual branch instruction given a |
| // (range-index, deadline) pair. |
| // |
| // |
| template <size_t SliceSize, size_t InstSize, class Inst, class Asm, |
| unsigned NumShortBranchRanges = 0> |
| struct AssemblerBufferWithConstantPools : public AssemblerBuffer<SliceSize, Inst> |
| { |
| private: |
| // The PoolEntry index counter. Each PoolEntry is given a unique index, |
| // counting up from zero, and these can be mapped back to the actual pool |
| // entry offset after finishing the buffer, see poolEntryOffset(). |
| size_t poolEntryCount; |
| |
| public: |
| class PoolEntry |
| { |
| size_t index_; |
| |
| public: |
| explicit PoolEntry(size_t index) |
| : index_(index) |
| { } |
| |
| PoolEntry() |
| : index_(-1) |
| { } |
| |
| size_t index() const { |
| return index_; |
| } |
| }; |
| |
| private: |
| typedef AssemblerBuffer<SliceSize, Inst> Parent; |
| using typename Parent::Slice; |
| |
| // The size of a pool guard, in instructions. A branch around the pool. |
| const unsigned guardSize_; |
| // The size of the header that is put at the beginning of a full pool, in |
| // instruction sized units. |
| const unsigned headerSize_; |
| |
| // The maximum pc relative offset encoded in instructions that reference |
| // pool entries. This is generally set to the maximum offset that can be |
| // encoded by the instructions, but for testing can be lowered to affect the |
| // pool placement and frequency of pool placement. |
| const size_t poolMaxOffset_; |
| |
| // The bias on pc relative addressing mode offsets, in units of bytes. The |
| // ARM has a bias of 8 bytes. |
| const unsigned pcBias_; |
| |
| // The current working pool. Copied out as needed before resetting. |
| Pool pool_; |
| |
| // The buffer should be aligned to this address. |
| const size_t instBufferAlign_; |
| |
| struct PoolInfo { |
| // The index of the first entry in this pool. |
| // Pool entries are numbered uniquely across all pools, starting from 0. |
| unsigned firstEntryIndex; |
| |
| // The location of this pool's first entry in the main assembler buffer. |
| // Note that the pool guard and header come before this offset which |
| // points directly at the data. |
| BufferOffset offset; |
| |
| explicit PoolInfo(unsigned index, BufferOffset data) |
| : firstEntryIndex(index) |
| , offset(data) |
| { |
| } |
| }; |
| |
| // Info for each pool that has already been dumped. This does not include |
| // any entries in pool_. |
| Vector<PoolInfo, 8, LifoAllocPolicy<Fallible>> poolInfo_; |
| |
| // Set of short-range forward branches that have not yet been bound. |
| // We may need to insert veneers if the final label turns out to be out of |
| // range. |
| // |
| // This set stores (rangeIdx, deadline) pairs instead of the actual branch |
| // locations. |
| BranchDeadlineSet<NumShortBranchRanges> branchDeadlines_; |
| |
| // When true dumping pools is inhibited. |
| bool canNotPlacePool_; |
| |
| #ifdef DEBUG |
| // State for validating the 'maxInst' argument to enterNoPool(). |
| // The buffer offset when entering the no-pool region. |
| size_t canNotPlacePoolStartOffset_; |
| // The maximum number of word sized instructions declared for the no-pool |
| // region. |
| size_t canNotPlacePoolMaxInst_; |
| #endif |
| |
| // Instruction to use for alignment fill. |
| const uint32_t alignFillInst_; |
| |
| // Insert a number of NOP instructions between each requested instruction at |
| // all locations at which a pool can potentially spill. This is useful for |
| // checking that instruction locations are correctly referenced and/or |
| // followed. |
| const uint32_t nopFillInst_; |
| const unsigned nopFill_; |
| // For inhibiting the insertion of fill NOPs in the dynamic context in which |
| // they are being inserted. |
| bool inhibitNops_; |
| |
| public: |
| // A unique id within each JitContext, to identify pools in the debug |
| // spew. Set by the MacroAssembler, see getNextAssemblerId(). |
| int id; |
| |
| private: |
| // The buffer slices are in a double linked list. |
| Slice* getHead() const { |
| return this->head; |
| } |
| Slice* getTail() const { |
| return this->tail; |
| } |
| |
| public: |
| // Create an assembler buffer. |
| // Note that this constructor is not allowed to actually allocate memory from this->lifoAlloc_ |
| // because the MacroAssembler constructor has not yet created an AutoJitContextAlloc. |
| AssemblerBufferWithConstantPools(unsigned guardSize, unsigned headerSize, |
| size_t instBufferAlign, size_t poolMaxOffset, |
| unsigned pcBias, uint32_t alignFillInst, uint32_t nopFillInst, |
| unsigned nopFill = 0) |
| : poolEntryCount(0), |
| guardSize_(guardSize), |
| headerSize_(headerSize), |
| poolMaxOffset_(poolMaxOffset), |
| pcBias_(pcBias), |
| pool_(poolMaxOffset, pcBias, this->lifoAlloc_), |
| instBufferAlign_(instBufferAlign), |
| poolInfo_(this->lifoAlloc_), |
| branchDeadlines_(this->lifoAlloc_), |
| canNotPlacePool_(false), |
| #ifdef DEBUG |
| canNotPlacePoolStartOffset_(0), |
| canNotPlacePoolMaxInst_(0), |
| #endif |
| alignFillInst_(alignFillInst), |
| nopFillInst_(nopFillInst), |
| nopFill_(nopFill), |
| inhibitNops_(false), |
| id(-1) |
| { } |
| |
| // We need to wait until an AutoJitContextAlloc is created by the |
| // MacroAssembler before allocating any space. |
| void initWithAllocator() { |
| // We hand out references to lifoAlloc_ in the constructor. |
| // Check that no allocations were made then. |
| MOZ_ASSERT(this->lifoAlloc_.isEmpty(), "Illegal LIFO allocations before AutoJitContextAlloc"); |
| } |
| |
| private: |
| size_t sizeExcludingCurrentPool() const { |
| // Return the actual size of the buffer, excluding the current pending |
| // pool. |
| return this->nextOffset().getOffset(); |
| } |
| |
| public: |
| size_t size() const { |
| // Return the current actual size of the buffer. This is only accurate |
| // if there are no pending pool entries to dump, check. |
| MOZ_ASSERT_IF(!this->oom(), pool_.numEntries() == 0); |
| return sizeExcludingCurrentPool(); |
| } |
| |
| private: |
| void insertNopFill() { |
| // Insert fill for testing. |
| if (nopFill_ > 0 && !inhibitNops_ && !canNotPlacePool_) { |
| inhibitNops_ = true; |
| |
| // Fill using a branch-nop rather than a NOP so this can be |
| // distinguished and skipped. |
| for (size_t i = 0; i < nopFill_; i++) |
| putInt(nopFillInst_); |
| |
| inhibitNops_ = false; |
| } |
| } |
| |
| static const unsigned OOM_FAIL = unsigned(-1); |
| static const unsigned NO_DATA = unsigned(-2); |
| |
| // Check if it is possible to add numInst instructions and numPoolEntries |
| // constant pool entries without needing to flush the current pool. |
| bool hasSpaceForInsts(unsigned numInsts, unsigned numPoolEntries) const |
| { |
| size_t nextOffset = sizeExcludingCurrentPool(); |
| // Earliest starting offset for the current pool after adding numInsts. |
| // This is the beginning of the pool entries proper, after inserting a |
| // guard branch + pool header. |
| size_t poolOffset = nextOffset + (numInsts + guardSize_ + headerSize_) * InstSize; |
| |
| // Any constant pool loads that would go out of range? |
| if (pool_.checkFull(poolOffset)) |
| return false; |
| |
| // Any short-range branch that would go out of range? |
| if (!branchDeadlines_.empty()) { |
| size_t deadline = branchDeadlines_.earliestDeadline().getOffset(); |
| size_t poolEnd = |
| poolOffset + pool_.getPoolSize() + numPoolEntries * sizeof(PoolAllocUnit); |
| |
| // When NumShortBranchRanges > 1, is is possible for branch deadlines to expire faster |
| // than we can insert veneers. Suppose branches are 4 bytes each, we could have the |
| // following deadline set: |
| // |
| // Range 0: 40, 44, 48 |
| // Range 1: 44, 48 |
| // |
| // It is not good enough to start inserting veneers at the 40 deadline; we would not be |
| // able to create veneers for the second 44 deadline. Instead, we need to start at 32: |
| // |
| // 32: veneer(40) |
| // 36: veneer(44) |
| // 40: veneer(44) |
| // 44: veneer(48) |
| // 48: veneer(48) |
| // |
| // This is a pretty conservative solution to the problem: If we begin at the earliest |
| // deadline, we can always emit all veneers for the range that currently has the most |
| // pending deadlines. That may not leave room for veneers for the remaining ranges, so |
| // reserve space for those secondary range veneers assuming the worst case deadlines. |
| |
| // Total pending secondary range veneer size. |
| size_t secondaryVeneers = |
| guardSize_ * (branchDeadlines_.size() - branchDeadlines_.maxRangeSize()); |
| |
| if (deadline < poolEnd + secondaryVeneers) |
| return false; |
| } |
| |
| return true; |
| } |
| |
| unsigned insertEntryForwards(unsigned numInst, unsigned numPoolEntries, uint8_t* inst, uint8_t* data) { |
| // If inserting pool entries then find a new limiter before we do the |
| // range check. |
| if (numPoolEntries) |
| pool_.updateLimiter(BufferOffset(sizeExcludingCurrentPool())); |
| |
| if (!hasSpaceForInsts(numInst, numPoolEntries)) { |
| if (numPoolEntries) |
| JitSpew(JitSpew_Pools, "[%d] Inserting pool entry caused a spill", id); |
| else |
| JitSpew(JitSpew_Pools, "[%d] Inserting instruction(%d) caused a spill", id, |
| sizeExcludingCurrentPool()); |
| |
| finishPool(); |
| if (this->oom()) |
| return OOM_FAIL; |
| return insertEntryForwards(numInst, numPoolEntries, inst, data); |
| } |
| if (numPoolEntries) { |
| unsigned result = pool_.insertEntry(numPoolEntries, data, this->nextOffset(), this->lifoAlloc_); |
| if (result == Pool::OOM_FAIL) { |
| this->fail_oom(); |
| return OOM_FAIL; |
| } |
| return result; |
| } |
| |
| // The pool entry index is returned above when allocating an entry, but |
| // when not allocating an entry a dummy value is returned - it is not |
| // expected to be used by the caller. |
| return NO_DATA; |
| } |
| |
| public: |
| // Get the next buffer offset where an instruction would be inserted. |
| // This may flush the current constant pool before returning nextOffset(). |
| BufferOffset nextInstrOffset() |
| { |
| if (!hasSpaceForInsts(/* numInsts= */ 1, /* numPoolEntries= */ 0)) { |
| JitSpew(JitSpew_Pools, "[%d] nextInstrOffset @ %d caused a constant pool spill", id, |
| this->nextOffset().getOffset()); |
| finishPool(); |
| } |
| return this->nextOffset(); |
| } |
| |
| BufferOffset allocEntry(size_t numInst, unsigned numPoolEntries, |
| uint8_t* inst, uint8_t* data, PoolEntry* pe = nullptr, |
| bool markAsBranch = false) |
| { |
| // The alloction of pool entries is not supported in a no-pool region, |
| // check. |
| MOZ_ASSERT_IF(numPoolEntries, !canNotPlacePool_); |
| |
| if (this->oom() && !this->bail()) |
| return BufferOffset(); |
| |
| insertNopFill(); |
| |
| #ifdef JS_JITSPEW |
| if (numPoolEntries && JitSpewEnabled(JitSpew_Pools)) { |
| JitSpew(JitSpew_Pools, "[%d] Inserting %d entries into pool", id, numPoolEntries); |
| JitSpewStart(JitSpew_Pools, "[%d] data is: 0x", id); |
| size_t length = numPoolEntries * sizeof(PoolAllocUnit); |
| for (unsigned idx = 0; idx < length; idx++) { |
| JitSpewCont(JitSpew_Pools, "%02x", data[length - idx - 1]); |
| if (((idx & 3) == 3) && (idx + 1 != length)) |
| JitSpewCont(JitSpew_Pools, "_"); |
| } |
| JitSpewFin(JitSpew_Pools); |
| } |
| #endif |
| |
| // Insert the pool value. |
| unsigned index = insertEntryForwards(numInst, numPoolEntries, inst, data); |
| if (this->oom()) |
| return BufferOffset(); |
| |
| // Now to get an instruction to write. |
| PoolEntry retPE; |
| if (numPoolEntries) { |
| JitSpew(JitSpew_Pools, "[%d] Entry has index %u, offset %u", id, index, |
| sizeExcludingCurrentPool()); |
| Asm::InsertIndexIntoTag(inst, index); |
| // Figure out the offset within the pool entries. |
| retPE = PoolEntry(poolEntryCount); |
| poolEntryCount += numPoolEntries; |
| } |
| // Now inst is a valid thing to insert into the instruction stream. |
| if (pe != nullptr) |
| *pe = retPE; |
| return this->putBytes(numInst * InstSize, inst); |
| } |
| |
| BufferOffset putInt(uint32_t value, bool markAsBranch = false) { |
| return allocEntry(1, 0, (uint8_t*)&value, nullptr, nullptr, markAsBranch); |
| } |
| |
| // Register a short-range branch deadline. |
| // |
| // After inserting a short-range forward branch, call this method to |
| // register the branch 'deadline' which is the last buffer offset that the |
| // branch instruction can reach. |
| // |
| // When the branch is bound to a destination label, call |
| // unregisterBranchDeadline() to stop tracking this branch, |
| // |
| // If the assembled code is about to exceed the registered branch deadline, |
| // and unregisterBranchDeadline() has not yet been called, an |
| // instruction-sized constant pool entry is allocated before the branch |
| // deadline. |
| // |
| // rangeIdx |
| // A number < NumShortBranchRanges identifying the range of the branch. |
| // |
| // deadline |
| // The highest buffer offset the the short-range branch can reach |
| // directly. |
| // |
| void registerBranchDeadline(unsigned rangeIdx, BufferOffset deadline) |
| { |
| if (!this->oom() && !branchDeadlines_.addDeadline(rangeIdx, deadline)) |
| this->fail_oom(); |
| } |
| |
| // Un-register a short-range branch deadline. |
| // |
| // When a short-range branch has been successfully bound to its destination |
| // label, call this function to stop traching the branch. |
| // |
| // The (rangeIdx, deadline) pair must be previously registered. |
| // |
| void unregisterBranchDeadline(unsigned rangeIdx, BufferOffset deadline) |
| { |
| if (!this->oom()) |
| branchDeadlines_.removeDeadline(rangeIdx, deadline); |
| } |
| |
| private: |
| // Are any short-range branches about to expire? |
| bool hasExpirableShortRangeBranches() const |
| { |
| if (branchDeadlines_.empty()) |
| return false; |
| |
| // Include branches that would expire in the next N bytes. |
| // The hysteresis avoids the needless creation of many tiny constant |
| // pools. |
| return this->nextOffset().getOffset() + ShortRangeBranchHysteresis > |
| size_t(branchDeadlines_.earliestDeadline().getOffset()); |
| } |
| |
| void finishPool() { |
| JitSpew(JitSpew_Pools, "[%d] Attempting to finish pool %d with %d entries.", id, |
| poolInfo_.length(), pool_.numEntries()); |
| |
| if (pool_.numEntries() == 0 && !hasExpirableShortRangeBranches()) { |
| // If there is no data in the pool being dumped, don't dump anything. |
| JitSpew(JitSpew_Pools, "[%d] Aborting because the pool is empty", id); |
| return; |
| } |
| |
| // Should not be placing a pool in a no-pool region, check. |
| MOZ_ASSERT(!canNotPlacePool_); |
| |
| // Dump the pool with a guard branch around the pool. |
| BufferOffset guard = this->putBytes(guardSize_ * InstSize, nullptr); |
| BufferOffset header = this->putBytes(headerSize_ * InstSize, nullptr); |
| BufferOffset data = |
| this->putBytesLarge(pool_.getPoolSize(), (const uint8_t*)pool_.poolData()); |
| if (this->oom()) |
| return; |
| |
| // Now generate branch veneers for any short-range branches that are |
| // about to expire. |
| while (hasExpirableShortRangeBranches()) { |
| unsigned rangeIdx = branchDeadlines_.earliestDeadlineRange(); |
| BufferOffset deadline = branchDeadlines_.earliestDeadline(); |
| |
| // Stop tracking this branch. The Asm callback below may register |
| // new branches to track. |
| branchDeadlines_.removeDeadline(rangeIdx, deadline); |
| |
| // Make room for the veneer. Same as a pool guard branch. |
| BufferOffset veneer = this->putBytes(guardSize_ * InstSize, nullptr); |
| if (this->oom()) |
| return; |
| |
| // Fix the branch so it targets the veneer. |
| // The Asm class knows how to find the original branch given the |
| // (rangeIdx, deadline) pair. |
| Asm::PatchShortRangeBranchToVeneer(this, rangeIdx, deadline, veneer); |
| } |
| |
| // We only reserved space for the guard branch and pool header. |
| // Fill them in. |
| BufferOffset afterPool = this->nextOffset(); |
| Asm::WritePoolGuard(guard, this->getInst(guard), afterPool); |
| Asm::WritePoolHeader((uint8_t*)this->getInst(header), &pool_, false); |
| |
| // With the pool's final position determined it is now possible to patch |
| // the instructions that reference entries in this pool, and this is |
| // done incrementally as each pool is finished. |
| size_t poolOffset = data.getOffset(); |
| |
| unsigned idx = 0; |
| for (BufferOffset* iter = pool_.loadOffsets.begin(); |
| iter != pool_.loadOffsets.end(); |
| ++iter, ++idx) |
| { |
| // All entries should be before the pool. |
| MOZ_ASSERT(iter->getOffset() < guard.getOffset()); |
| |
| // Everything here is known so we can safely do the necessary |
| // substitutions. |
| Inst* inst = this->getInst(*iter); |
| size_t codeOffset = poolOffset - iter->getOffset(); |
| |
| // That is, PatchConstantPoolLoad wants to be handed the address of |
| // the pool entry that is being loaded. We need to do a non-trivial |
| // amount of math here, since the pool that we've made does not |
| // actually reside there in memory. |
| JitSpew(JitSpew_Pools, "[%d] Fixing entry %d offset to %u", id, idx, codeOffset); |
| Asm::PatchConstantPoolLoad(inst, (uint8_t*)inst + codeOffset); |
| } |
| |
| // Record the pool info. |
| unsigned firstEntry = poolEntryCount - pool_.numEntries(); |
| if (!poolInfo_.append(PoolInfo(firstEntry, data))) { |
| this->fail_oom(); |
| return; |
| } |
| |
| // Reset everything to the state that it was in when we started. |
| pool_.reset(); |
| } |
| |
| public: |
| void flushPool() { |
| if (this->oom()) |
| return; |
| JitSpew(JitSpew_Pools, "[%d] Requesting a pool flush", id); |
| finishPool(); |
| } |
| |
| void enterNoPool(size_t maxInst) { |
| // Don't allow re-entry. |
| MOZ_ASSERT(!canNotPlacePool_); |
| insertNopFill(); |
| |
| // Check if the pool will spill by adding maxInst instructions, and if |
| // so then finish the pool before entering the no-pool region. It is |
| // assumed that no pool entries are allocated in a no-pool region and |
| // this is asserted when allocating entries. |
| if (!hasSpaceForInsts(maxInst, 0)) { |
| JitSpew(JitSpew_Pools, "[%d] No-Pool instruction(%d) caused a spill.", id, |
| sizeExcludingCurrentPool()); |
| finishPool(); |
| } |
| |
| #ifdef DEBUG |
| // Record the buffer position to allow validating maxInst when leaving |
| // the region. |
| canNotPlacePoolStartOffset_ = this->nextOffset().getOffset(); |
| canNotPlacePoolMaxInst_ = maxInst; |
| #endif |
| |
| canNotPlacePool_ = true; |
| } |
| |
| void leaveNoPool() { |
| MOZ_ASSERT(canNotPlacePool_); |
| canNotPlacePool_ = false; |
| |
| // Validate the maxInst argument supplied to enterNoPool(). |
| MOZ_ASSERT(this->nextOffset().getOffset() - canNotPlacePoolStartOffset_ <= canNotPlacePoolMaxInst_ * InstSize); |
| } |
| |
| void align(unsigned alignment) { |
| MOZ_ASSERT(IsPowerOfTwo(alignment) && alignment >= InstSize); |
| |
| // A pool many need to be dumped at this point, so insert NOP fill here. |
| insertNopFill(); |
| |
| // Check if the code position can be aligned without dumping a pool. |
| unsigned requiredFill = sizeExcludingCurrentPool() & (alignment - 1); |
| if (requiredFill == 0) |
| return; |
| requiredFill = alignment - requiredFill; |
| |
| // Add an InstSize because it is probably not useful for a pool to be |
| // dumped at the aligned code position. |
| if (!hasSpaceForInsts(requiredFill / InstSize + 1, 0)) { |
| // Alignment would cause a pool dump, so dump the pool now. |
| JitSpew(JitSpew_Pools, "[%d] Alignment of %d at %d caused a spill.", id, alignment, |
| sizeExcludingCurrentPool()); |
| finishPool(); |
| } |
| |
| inhibitNops_ = true; |
| while ((sizeExcludingCurrentPool() & (alignment - 1)) && !this->oom()) |
| putInt(alignFillInst_); |
| inhibitNops_ = false; |
| } |
| |
| public: |
| void executableCopy(uint8_t* dest) { |
| if (this->oom()) |
| return; |
| // The pools should have all been flushed, check. |
| MOZ_ASSERT(pool_.numEntries() == 0); |
| for (Slice* cur = getHead(); cur != nullptr; cur = cur->getNext()) { |
| memcpy(dest, &cur->instructions[0], cur->length()); |
| dest += cur->length(); |
| } |
| } |
| |
| bool appendBuffer(const AssemblerBufferWithConstantPools& other) { |
| if (this->oom()) |
| return false; |
| // The pools should have all been flushed, check. |
| MOZ_ASSERT(pool_.numEntries() == 0); |
| for (Slice* cur = other.getHead(); cur != nullptr; cur = cur->getNext()) { |
| this->putBytes(cur->length(), &cur->instructions[0]); |
| if (this->oom()) |
| return false; |
| } |
| return true; |
| } |
| |
| public: |
| size_t poolEntryOffset(PoolEntry pe) const { |
| MOZ_ASSERT(pe.index() < poolEntryCount - pool_.numEntries(), |
| "Invalid pool entry, or not flushed yet."); |
| // Find the pool containing pe.index(). |
| // The array is sorted, so we can use a binary search. |
| auto b = poolInfo_.begin(), e = poolInfo_.end(); |
| // A note on asymmetric types in the upper_bound comparator: |
| // http://permalink.gmane.org/gmane.comp.compilers.clang.devel/10101 |
| auto i = std::upper_bound(b, e, pe.index(), [](size_t value, const PoolInfo& entry) { |
| return value < entry.firstEntryIndex; |
| }); |
| // Since upper_bound finds the first pool greater than pe, |
| // we want the previous one which is the last one less than or equal. |
| MOZ_ASSERT(i != b, "PoolInfo not sorted or empty?"); |
| --i; |
| // The i iterator now points to the pool containing pe.index. |
| MOZ_ASSERT(i->firstEntryIndex <= pe.index() && |
| (i + 1 == e || (i + 1)->firstEntryIndex > pe.index())); |
| // Compute the byte offset into the pool. |
| unsigned relativeIndex = pe.index() - i->firstEntryIndex; |
| return i->offset.getOffset() + relativeIndex * sizeof(PoolAllocUnit); |
| } |
| }; |
| |
| } // namespace ion |
| } // namespace js |
| |
| #endif // jit_shared_IonAssemblerBufferWithConstantPools_h |