libredex/Purity.cpp (788 lines of code) (raw):

/* * Copyright (c) Meta Platforms, Inc. and affiliates. * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ #include "Purity.h" #include <sstream> #include "ConfigFiles.h" #include "ControlFlow.h" #include "EditableCfgAdapter.h" #include "IRInstruction.h" #include "Resolver.h" #include "Show.h" #include "StlUtil.h" #include "Trace.h" #include "Walkers.h" #include "WeakTopologicalOrdering.h" namespace purity { CacheConfig& CacheConfig::get_default() { static CacheConfig cc = CacheConfig{}; return cc; } void CacheConfig::parse_default(const ConfigFiles& conf) { CacheConfig& def = CacheConfig::get_default(); const auto& json = conf.get_json_config(); if (json.contains("purity")) { auto purity = JsonWrapper(json["purity"]); if (purity.contains("cache")) { auto cache = JsonWrapper(purity["cache"]); cache.get("max_entries", def.max_entries, def.max_entries); cache.get("fill_entry_threshold", def.fill_entry_threshold, def.fill_entry_threshold); cache.get("fill_size_threshold", def.fill_size_threshold, def.fill_size_threshold); } } } } // namespace purity std::ostream& operator<<(std::ostream& o, const CseLocation& l) { switch (l.special_location) { case CseSpecialLocations::GENERAL_MEMORY_BARRIER: o << "*"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_INT: o << "(int[])[.]"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_BYTE: o << "(byte[])[.]"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_CHAR: o << "(char[])[.]"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_WIDE: o << "(long|double[])[.]"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_SHORT: o << "(short[])[.]"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_OBJECT: o << "(Object[])[.]"; break; case CseSpecialLocations::ARRAY_COMPONENT_TYPE_BOOLEAN: o << "(boolean[])[.]"; break; default: o << SHOW(l.field); break; } return o; } std::ostream& operator<<(std::ostream& o, const CseUnorderedLocationSet& ls) { o << "{"; bool first = true; for (const auto& l : ls) { if (first) { first = false; } else { o << ", "; } o << l; } o << "}"; return o; } CseLocation get_field_location(IROpcode opcode, const DexField* field) { always_assert(opcode::is_an_ifield_op(opcode) || opcode::is_an_sfield_op(opcode)); if (field != nullptr && !is_volatile(field)) { return CseLocation(field); } return CseLocation(CseSpecialLocations::GENERAL_MEMORY_BARRIER); } CseLocation get_field_location(IROpcode opcode, const DexFieldRef* field_ref) { always_assert(opcode::is_an_ifield_op(opcode) || opcode::is_an_sfield_op(opcode)); DexField* field = resolve_field(field_ref, opcode::is_an_sfield_op(opcode) ? FieldSearch::Static : FieldSearch::Instance); return get_field_location(opcode, field); } CseLocation get_read_array_location(IROpcode opcode) { switch (opcode) { case OPCODE_AGET: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_INT); case OPCODE_AGET_BYTE: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_BYTE); case OPCODE_AGET_CHAR: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_CHAR); case OPCODE_AGET_WIDE: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_WIDE); case OPCODE_AGET_SHORT: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_SHORT); case OPCODE_AGET_OBJECT: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_OBJECT); case OPCODE_AGET_BOOLEAN: return CseLocation(CseSpecialLocations::ARRAY_COMPONENT_TYPE_BOOLEAN); default: not_reached(); } } CseLocation get_read_location(const IRInstruction* insn) { if (opcode::is_an_aget(insn->opcode())) { return get_read_array_location(insn->opcode()); } else if (opcode::is_an_iget(insn->opcode()) || opcode::is_an_sget(insn->opcode())) { return get_field_location(insn->opcode(), insn->get_field()); } else { return CseLocation(CseSpecialLocations::GENERAL_MEMORY_BARRIER); } } static const char* pure_method_names[] = { "Ljava/lang/Boolean;.booleanValue:()Z", "Ljava/lang/Boolean;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Boolean;.getBoolean:(Ljava/lang/String;)Z", "Ljava/lang/Boolean;.hashCode:()I", "Ljava/lang/Boolean;.toString:()Ljava/lang/String;", "Ljava/lang/Boolean;.toString:(Z)Ljava/lang/String;", "Ljava/lang/Boolean;.valueOf:(Z)Ljava/lang/Boolean;", "Ljava/lang/Boolean;.valueOf:(Ljava/lang/String;)Ljava/lang/Boolean;", "Ljava/lang/Byte;.byteValue:()B", "Ljava/lang/Byte;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Byte;.toString:()Ljava/lang/String;", "Ljava/lang/Byte;.toString:(B)Ljava/lang/String;", "Ljava/lang/Byte;.valueOf:(B)Ljava/lang/Byte;", "Ljava/lang/Character;.valueOf:(C)Ljava/lang/Character;", "Ljava/lang/Character;.charValue:()C", "Ljava/lang/Class;.getName:()Ljava/lang/String;", "Ljava/lang/Class;.getSimpleName:()Ljava/lang/String;", "Ljava/lang/Double;.compare:(DD)I", "Ljava/lang/Double;.doubleValue:()D", "Ljava/lang/Double;.doubleToLongBits:(D)J", "Ljava/lang/Double;.doubleToRawLongBits:(D)J", "Ljava/lang/Double;.floatValue:()F", "Ljava/lang/Double;.hashCode:()I", "Ljava/lang/Double;.intValue:()I", "Ljava/lang/Double;.isInfinite:(D)Z", "Ljava/lang/Double;.isNaN:(D)Z", "Ljava/lang/Double;.longBitsToDouble:(J)D", "Ljava/lang/Double;.longValue:()J", "Ljava/lang/Double;.toString:(D)Ljava/lang/String;", "Ljava/lang/Double;.valueOf:(D)Ljava/lang/Double;", "Ljava/lang/Enum;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Enum;.name:()Ljava/lang/String;", "Ljava/lang/Enum;.ordinal:()I", "Ljava/lang/Enum;.toString:()Ljava/lang/String;", "Ljava/lang/Float;.doubleValue:()D", "Ljava/lang/Float;.floatToRawIntBits:(F)I", "Ljava/lang/Float;.floatValue:()F", "Ljava/lang/Float;.compare:(FF)I", "Ljava/lang/Float;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Float;.hashCode:()I", "Ljava/lang/Float;.intBitsToFloat:(I)F", "Ljava/lang/Float;.intValue:()I", "Ljava/lang/Float;.floatToIntBits:(F)I", "Ljava/lang/Float;.isInfinite:(F)Z", "Ljava/lang/Float;.isNaN:(F)Z", "Ljava/lang/Float;.valueOf:(F)Ljava/lang/Float;", "Ljava/lang/Float;.toString:(F)Ljava/lang/String;", "Ljava/lang/Integer;.bitCount:(I)I", "Ljava/lang/Integer;.byteValue:()B", "Ljava/lang/Integer;.compareTo:(Ljava/lang/Integer;)I", "Ljava/lang/Integer;.doubleValue:()D", "Ljava/lang/Integer;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Integer;.hashCode:()I", "Ljava/lang/Integer;.highestOneBit:(I)I", "Ljava/lang/Integer;.intValue:()I", "Ljava/lang/Integer;.longValue:()J", "Ljava/lang/Integer;.lowestOneBit:(I)I", "Ljava/lang/Integer;.numberOfLeadingZeros:(I)I", "Ljava/lang/Integer;.numberOfTrailingZeros:(I)I", "Ljava/lang/Integer;.shortValue:()S", "Ljava/lang/Integer;.signum:(I)I", "Ljava/lang/Integer;.toBinaryString:(I)Ljava/lang/String;", "Ljava/lang/Integer;.toHexString:(I)Ljava/lang/String;", "Ljava/lang/Integer;.toString:()Ljava/lang/String;", "Ljava/lang/Integer;.toString:(I)Ljava/lang/String;", "Ljava/lang/Integer;.toString:(II)Ljava/lang/String;", "Ljava/lang/Integer;.valueOf:(I)Ljava/lang/Integer;", "Ljava/lang/Long;.bitCount:(J)I", "Ljava/lang/Long;.compareTo:(Ljava/lang/Long;)I", "Ljava/lang/Long;.doubleValue:()D", "Ljava/lang/Long;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Long;.hashCode:()I", "Ljava/lang/Long;.intValue:()I", "Ljava/lang/Long;.highestOneBit:(J)J", "Ljava/lang/Long;.longValue:()J", "Ljava/lang/Long;.numberOfTrailingZeros:(J)I", "Ljava/lang/Long;.signum:(J)I", "Ljava/lang/Long;.toBinaryString:(J)Ljava/lang/String;", "Ljava/lang/Long;.toHexString:(J)Ljava/lang/String;", "Ljava/lang/Long;.toString:()Ljava/lang/String;", "Ljava/lang/Long;.toString:(J)Ljava/lang/String;", "Ljava/lang/Long;.valueOf:(J)Ljava/lang/Long;", "Ljava/lang/Math;.IEEEremainder:(DD)D", "Ljava/lang/Math;.abs:(J)J", "Ljava/lang/Math;.abs:(I)I", "Ljava/lang/Math;.abs:(F)F", "Ljava/lang/Math;.abs:(D)D", "Ljava/lang/Math;.acos:(D)D", "Ljava/lang/Math;.asin:(D)D", "Ljava/lang/Math;.atan:(D)D", "Ljava/lang/Math;.atan2:(DD)D", "Ljava/lang/Math;.cbrt:(D)D", "Ljava/lang/Math;.ceil:(D)D", "Ljava/lang/Math;.copySign:(FF)F", "Ljava/lang/Math;.copySign:(DD)D", "Ljava/lang/Math;.cos:(D)D", "Ljava/lang/Math;.cosh:(D)D", "Ljava/lang/Math;.exp:(D)D", "Ljava/lang/Math;.expm1:(D)D", "Ljava/lang/Math;.floor:(D)D", "Ljava/lang/Math;.floorDiv:(II)I", "Ljava/lang/Math;.floorDiv:(JJ)J", "Ljava/lang/Math;.floorMod:(JJ)J", "Ljava/lang/Math;.floorMod:(II)I", "Ljava/lang/Math;.getExponent:(D)I", "Ljava/lang/Math;.getExponent:(F)I", "Ljava/lang/Math;.hypot:(DD)D", "Ljava/lang/Math;.log:(D)D", "Ljava/lang/Math;.log10:(D)D", "Ljava/lang/Math;.log1p:(D)D", "Ljava/lang/Math;.max:(II)I", "Ljava/lang/Math;.max:(JJ)J", "Ljava/lang/Math;.max:(FF)F", "Ljava/lang/Math;.max:(DD)D", "Ljava/lang/Math;.min:(FF)F", "Ljava/lang/Math;.min:(DD)D", "Ljava/lang/Math;.min:(II)I", "Ljava/lang/Math;.min:(JJ)J", "Ljava/lang/Math;.nextAfter:(DD)D", "Ljava/lang/Math;.nextAfter:(FD)F", "Ljava/lang/Math;.nextDown:(D)D", "Ljava/lang/Math;.nextDown:(F)F", "Ljava/lang/Math;.nextUp:(F)F", "Ljava/lang/Math;.nextUp:(D)D", "Ljava/lang/Math;.pow:(DD)D", "Ljava/lang/Math;.random:()D", "Ljava/lang/Math;.rint:(D)D", "Ljava/lang/Math;.round:(D)J", "Ljava/lang/Math;.round:(F)I", "Ljava/lang/Math;.scalb:(FI)F", "Ljava/lang/Math;.scalb:(DI)D", "Ljava/lang/Math;.signum:(D)D", "Ljava/lang/Math;.signum:(F)F", "Ljava/lang/Math;.sin:(D)D", "Ljava/lang/Math;.sinh:(D)D", "Ljava/lang/Math;.sqrt:(D)D", "Ljava/lang/Math;.tan:(D)D", "Ljava/lang/Math;.tanh:(D)D", "Ljava/lang/Math;.toDegrees:(D)D", "Ljava/lang/Math;.toRadians:(D)D", "Ljava/lang/Math;.ulp:(D)D", "Ljava/lang/Math;.ulp:(F)F", "Ljava/lang/Object;.getClass:()Ljava/lang/Class;", "Ljava/lang/Short;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/Short;.shortValue:()S", "Ljava/lang/Short;.toString:(S)Ljava/lang/String;", "Ljava/lang/Short;.valueOf:(S)Ljava/lang/Short;", "Ljava/lang/String;.compareTo:(Ljava/lang/String;)I", "Ljava/lang/String;.compareToIgnoreCase:(Ljava/lang/String;)I", "Ljava/lang/String;.concat:(Ljava/lang/String;)Ljava/lang/String;", "Ljava/lang/String;.endsWith:(Ljava/lang/String;)Z", "Ljava/lang/String;.equals:(Ljava/lang/Object;)Z", "Ljava/lang/String;.equalsIgnoreCase:(Ljava/lang/String;)Z", "Ljava/lang/String;.hashCode:()I", "Ljava/lang/String;.indexOf:(I)I", "Ljava/lang/String;.isEmpty:()Z", "Ljava/lang/String;.indexOf:(Ljava/lang/String;)I", "Ljava/lang/String;.indexOf:(II)I", "Ljava/lang/String;.indexOf:(Ljava/lang/String;I)I", "Ljava/lang/String;.lastIndexOf:(I)I", "Ljava/lang/String;.lastIndexOf:(II)I", "Ljava/lang/String;.lastIndexOf:(Ljava/lang/String;)I", "Ljava/lang/String;.lastIndexOf:(Ljava/lang/String;I)I", "Ljava/lang/String;.length:()I", "Ljava/lang/String;.replace:(CC)Ljava/lang/String;", "Ljava/lang/String;.startsWith:(Ljava/lang/String;)Z", "Ljava/lang/String;.startsWith:(Ljava/lang/String;I)Z", "Ljava/lang/String;.toLowerCase:()Ljava/lang/String;", "Ljava/lang/String;.toLowerCase:(Ljava/util/Locale;)Ljava/lang/String;", "Ljava/lang/String;.toString:()Ljava/lang/String;", "Ljava/lang/String;.toUpperCase:()Ljava/lang/String;", "Ljava/lang/String;.toUpperCase:(Ljava/util/Locale;)Ljava/lang/String;", "Ljava/lang/String;.trim:()Ljava/lang/String;", "Ljava/lang/String;.valueOf:(C)Ljava/lang/String;", "Ljava/lang/String;.valueOf:(D)Ljava/lang/String;", "Ljava/lang/String;.valueOf:(F)Ljava/lang/String;", "Ljava/lang/String;.valueOf:(I)Ljava/lang/String;", "Ljava/lang/String;.valueOf:(J)Ljava/lang/String;", "Ljava/lang/String;.valueOf:(Z)Ljava/lang/String;", "Ljava/lang/System;.identityHashCode:(Ljava/lang/Object;)I", "Ljava/lang/Thread;.currentThread:()Ljava/lang/Thread;", }; std::unordered_set<DexMethodRef*> get_pure_methods() { std::unordered_set<DexMethodRef*> pure_methods; for (auto const pure_method_name : pure_method_names) { auto method_ref = DexMethod::get_method(std::string(pure_method_name)); if (method_ref == nullptr) { TRACE(CSE, 1, "[get_pure_methods]: Could not find pure method %s", pure_method_name); continue; } pure_methods.insert(method_ref); } return pure_methods; } std::unordered_set<DexMethod*> get_immutable_getters(const Scope& scope) { std::unordered_set<DexMethod*> pure_methods; walk::methods(scope, [&](DexMethod* method) { if (method->rstate.immutable_getter()) { pure_methods.insert(method); } }); return pure_methods; } MethodOverrideAction get_base_or_overriding_method_action( const DexMethod* method, const std::unordered_set<const DexMethod*>* methods_to_ignore, bool ignore_methods_with_assumenosideeffects) { if (method == nullptr || method::is_clinit(method) || method->rstate.no_optimizations()) { return MethodOverrideAction::UNKNOWN; } if ((method->is_virtual() && is_interface(type_class(method->get_class()))) && (root(method) || !can_rename(method))) { // We cannot rule out that there are dynamically added classes, created via // Proxy.newProxyInstance, that override this method. // So we assume the worst. return MethodOverrideAction::UNKNOWN; } if (methods_to_ignore && methods_to_ignore->count(method)) { return MethodOverrideAction::EXCLUDE; } if (ignore_methods_with_assumenosideeffects && assumenosideeffects(method)) { return MethodOverrideAction::EXCLUDE; } if (method->is_external() || is_native(method)) { return MethodOverrideAction::UNKNOWN; } if (is_abstract(method)) { return MethodOverrideAction::EXCLUDE; } return MethodOverrideAction::INCLUDE; } bool process_base_and_overriding_methods( const method_override_graph::Graph* method_override_graph, const DexMethod* method, const std::unordered_set<const DexMethod*>* methods_to_ignore, bool ignore_methods_with_assumenosideeffects, const std::function<bool(DexMethod*)>& handler_func) { auto action = get_base_or_overriding_method_action( method, methods_to_ignore, ignore_methods_with_assumenosideeffects); if (action == MethodOverrideAction::UNKNOWN || (action == MethodOverrideAction::INCLUDE && !handler_func(const_cast<DexMethod*>(method)))) { return false; } // When the method isn't virtual, there are no overriden methods to consider. if (!method->is_virtual()) { return true; } // But even if there are overriden methods, don't look further when the // method is to be ignored. if (methods_to_ignore && methods_to_ignore->count(method)) { return true; } if (ignore_methods_with_assumenosideeffects && assumenosideeffects(method)) { return true; } // When we don't have a method-override graph, let's be conservative and give // up. if (!method_override_graph) { return false; } // Okay, let's process all overridden methods just like the base method. auto overriding_methods = method_override_graph::get_overriding_methods( *method_override_graph, method); for (auto overriding_method : overriding_methods) { action = get_base_or_overriding_method_action( overriding_method, methods_to_ignore, ignore_methods_with_assumenosideeffects); if (action == MethodOverrideAction::UNKNOWN || (action == MethodOverrideAction::INCLUDE && !handler_func(const_cast<DexMethod*>(overriding_method)))) { return false; } } return true; } size_t compute_locations_closure( const Scope& scope, const method_override_graph::Graph* method_override_graph, std::function<boost::optional<LocationsAndDependencies>(DexMethod*)> init_func, std::unordered_map<const DexMethod*, CseUnorderedLocationSet>* result, const purity::CacheConfig& cache_config) { // 1. Let's initialize known method read locations and dependencies by // scanning method bodies ConcurrentMap<const DexMethod*, LocationsAndDependencies> concurrent_method_lads; walk::parallel::methods(scope, [&](DexMethod* method) { auto lads = init_func(method); if (lads) { concurrent_method_lads.emplace(method, std::move(*lads)); } }); std::unordered_map<const DexMethod*, LocationsAndDependencies> method_lads; for (auto& p : concurrent_method_lads) { method_lads.insert(std::move(p)); } // 2. Compute inverse dependencies so that we know what needs to be recomputed // during the fixpoint computation, and determine set of methods that are // initially "impacted" in the sense that they have dependencies. std::unordered_map<const DexMethod*, std::unordered_set<const DexMethod*>> inverse_dependencies; std::unordered_set<const DexMethod*> impacted_methods; for (const auto& p : method_lads) { auto method = p.first; auto& lads = p.second; if (!lads.dependencies.empty()) { for (auto d : lads.dependencies) { inverse_dependencies[d].insert(method); } impacted_methods.insert(method); } } // 3. Let's try to (semantically) inline locations, computing a fixed // point. Methods for which information is directly or indirectly absent // are equivalent to a general memory barrier, and are systematically // pruned. // TODO: Instead of custom fixpoint computation using WTO, consider using the // MonotonicFixpointIterator, operating on a callgraph, capture the // dependencies, and have the Locations as the abstract domain. size_t iterations = 0; while (!impacted_methods.empty()) { iterations++; Timer t{std::string("Iteration ") + std::to_string(iterations)}; // We order the impacted methods in a deterministic way that's likely // helping to reduce the number of needed iterations. std::vector<const DexMethod*> ordered_impacted_methods; { constexpr bool kCacheDebug = false; struct Cache { struct CacheData { size_t runs{0}; size_t size_if_populated{0}; std::vector<const DexMethod*> data; }; std::unordered_map<const DexMethod*, CacheData> cache; size_t sum_entries{0}; size_t pop_miss{0}; const size_t cache_max_entries; const size_t cache_fill_entry_threshold; const size_t cache_fill_size_threshold; explicit Cache(const purity::CacheConfig& config) : cache_max_entries(config.max_entries), cache_fill_entry_threshold(config.fill_entry_threshold), cache_fill_size_threshold(config.fill_size_threshold) {} boost::optional<std::vector<const DexMethod*>> get(const DexMethod* m) { auto it = cache.find(m); if (it == cache.end()) { return boost::none; } redex_assert(it->second.runs > 0); if (it->second.runs < cache_fill_entry_threshold) { return boost::none; } ++it->second.runs; if (it->second.size_if_populated > 0) { // We did not have space. pop_miss++; return boost::none; } return it->second.data; } void new_data(const DexMethod* m, const std::vector<const DexMethod*>& data) { CacheData& cd = cache[m]; cd.runs++; if (cd.runs < cache_fill_entry_threshold) { return; // Do not cache, yet. } size_t size = data.size(); if (size >= cache_fill_size_threshold && sum_entries + size <= cache_max_entries) { redex_assert(cd.size_if_populated == 0); cd.data = data; sum_entries += size; } else { redex_assert(cd.size_if_populated == 0 || cd.size_if_populated == size); cd.size_if_populated = size; } } void print_stats() const { std::ostringstream oss; size_t sum_not_pop{0}; size_t max_not_pop{0}; size_t max_pop{0}; size_t max_not_pop_runs{0}; size_t max_pop_runs{0}; size_t below_size_thresh{0}; size_t runs_below_size_thresh{0}; for (const auto& p : cache) { sum_not_pop += p.second.size_if_populated; max_not_pop = std::max(max_not_pop, p.second.size_if_populated); if (p.second.size_if_populated > 0) { max_not_pop_runs = std::max(max_not_pop_runs, p.second.runs); if (p.second.size_if_populated < cache_fill_size_threshold) { ++below_size_thresh; runs_below_size_thresh += p.second.runs; } } else { max_pop_runs = std::max(max_pop_runs, p.second.runs); max_pop = std::max(max_pop, p.second.data.size()); } } oss << " In=" << sum_entries << " Miss=" << pop_miss << std::endl; oss << " CACHED: Max=" << max_pop << " Runs=" << max_pop_runs << std::endl; oss << " NCACHED: Sum=" << sum_not_pop << " Max=" << max_not_pop << " Runs=" << max_not_pop_runs << std::endl; oss << " NCACHED: #short=" << below_size_thresh << " Runs=" << runs_below_size_thresh; TRACE(PURITY, 1, "%s", oss.str().c_str()); } }; Cache cache(cache_config); sparta::WeakTopologicalOrdering<const DexMethod*> wto( nullptr, [&impacted_methods, &inverse_dependencies, &cache](const DexMethod* const& m) { auto cached = cache.get(m); if (cached && !kCacheDebug) { return std::move(*cached); } std::vector<const DexMethod*> successors; if (m == nullptr) { std::copy(impacted_methods.begin(), impacted_methods.end(), std::back_inserter(successors)); } else { auto it = inverse_dependencies.find(m); if (it != inverse_dependencies.end()) { for (auto n : it->second) { if (impacted_methods.count(n)) { successors.push_back(n); } } } } // Make number of iterations deterministic std::sort(successors.begin(), successors.end(), compare_dexmethods); if (kCacheDebug && cached) { redex_assert(*cached == successors); } cache.new_data(m, successors); return successors; }); if (traceEnabled(PURITY, 5)) { cache.print_stats(); } wto.visit_depth_first([&ordered_impacted_methods](const DexMethod* m) { if (m) { ordered_impacted_methods.push_back(m); } }); impacted_methods.clear(); } std::vector<const DexMethod*> changed_methods; for (const DexMethod* method : ordered_impacted_methods) { auto& lads = method_lads.at(method); bool unknown = false; size_t lads_locations_size = lads.locations.size(); for (const DexMethod* d : lads.dependencies) { if (d == method) { continue; } auto it = method_lads.find(d); if (it == method_lads.end()) { unknown = true; break; } const auto& other_locations = it->second.locations; lads.locations.insert(other_locations.begin(), other_locations.end()); } if (unknown || lads_locations_size < lads.locations.size()) { // something changed changed_methods.push_back(method); if (unknown) { method_lads.erase(method); } } } // Given set of changed methods, determine set of dependents for which // we need to re-run the analysis in another iteration. for (auto changed_method : changed_methods) { auto idit = inverse_dependencies.find(changed_method); if (idit == inverse_dependencies.end()) { continue; } // remove inverse dependency entries as appropriate auto& entries = idit->second; std20::erase_if(entries, [&](auto* m) { return !method_lads.count(m); }); if (entries.empty()) { // remove inverse dependency inverse_dependencies.erase(changed_method); } else { // add inverse dependencies entries to impacted methods impacted_methods.insert(entries.begin(), entries.end()); } } } // For all methods which have a known set of locations at this point, // persist that information for (auto& p : method_lads) { result->emplace(p.first, std::move(p.second.locations)); } return iterations; } // Helper function that invokes compute_locations_closure, providing initial // set of locations indicating whether a function only reads locations (and // doesn't write). Via additional flags it can be selected whether... // - [ignore_methods_with_assumenosideeffects] to ignore invoked methods that // are marked with assumenosideeffects // - [for_conditional_purity] instructions that rule out conditional purity // should cause methods to be treated like methods with unknown behavior; in // particular, this rules out instructions that create new object instances, // as those may leak, and thus multiple invocations of such a method could // never be reduced by CSE. // - [compute_locations] the actual locations that are being read are computed // and returned; if false, then an empty set indicates that a particular // function only reads (some unknown set of) locations. static size_t analyze_read_locations( const Scope& scope, const method_override_graph::Graph* method_override_graph, const method::ClInitHasNoSideEffectsPredicate& clinit_has_no_side_effects, const std::unordered_set<DexMethodRef*>& pure_methods, bool ignore_methods_with_assumenosideeffects, bool for_conditional_purity, bool compute_locations, std::unordered_map<const DexMethod*, CseUnorderedLocationSet>* result, const purity::CacheConfig& cache_config) { std::unordered_set<const DexMethod*> pure_methods_closure; for (auto pure_method_ref : pure_methods) { auto pure_method = pure_method_ref->as_def(); if (pure_method == nullptr) { continue; } pure_methods_closure.insert(pure_method); if (pure_method->is_virtual() && method_override_graph) { const auto overriding_methods = method_override_graph::get_overriding_methods(*method_override_graph, pure_method); pure_methods_closure.insert(overriding_methods.begin(), overriding_methods.end()); } } return compute_locations_closure( scope, method_override_graph, [&](DexMethod* method) -> boost::optional<LocationsAndDependencies> { auto action = get_base_or_overriding_method_action( method, &pure_methods_closure, ignore_methods_with_assumenosideeffects); if (action == MethodOverrideAction::UNKNOWN) { return boost::none; } LocationsAndDependencies lads; if (!process_base_and_overriding_methods( method_override_graph, method, &pure_methods_closure, ignore_methods_with_assumenosideeffects, [&](DexMethod* other_method) { if (other_method != method) { lads.dependencies.insert(other_method); } return true; })) { return boost::none; } if (action == MethodOverrideAction::EXCLUDE) { return lads; } bool unknown = false; editable_cfg_adapter::iterate_with_iterator( method->get_code(), [&](const IRList::iterator& it) { auto insn = it->insn; auto opcode = insn->opcode(); switch (opcode) { case OPCODE_MONITOR_ENTER: case OPCODE_MONITOR_EXIT: case OPCODE_FILL_ARRAY_DATA: case OPCODE_THROW: unknown = true; break; case IOPCODE_INIT_CLASS: unknown = true; break; case OPCODE_NEW_INSTANCE: if (for_conditional_purity || !clinit_has_no_side_effects(insn->get_type())) { unknown = true; } break; case OPCODE_NEW_ARRAY: case OPCODE_FILLED_NEW_ARRAY: if (for_conditional_purity) { unknown = true; } break; case OPCODE_INVOKE_SUPER: // TODO: Support properly. unknown = true; break; default: if (opcode::is_an_aput(opcode) || opcode::is_an_iput(opcode) || opcode::is_an_sput(opcode)) { unknown = true; } else if (opcode::is_an_aget(opcode) || opcode::is_an_iget(opcode) || opcode::is_an_sget(opcode)) { auto location = get_read_location(insn); if (location == CseLocation( CseSpecialLocations::GENERAL_MEMORY_BARRIER)) { unknown = true; } else { if (opcode::is_an_sget(opcode) && (!clinit_has_no_side_effects( location.get_field()->get_class()))) { unknown = true; } else if (compute_locations) { lads.locations.insert(location); } } } else if (opcode::is_an_invoke(opcode)) { auto invoke_method = resolve_method( insn->get_method(), opcode_to_search(opcode), method); if ((invoke_method && opcode::is_invoke_static(opcode) && (!clinit_has_no_side_effects( invoke_method->get_class()))) || !process_base_and_overriding_methods( method_override_graph, invoke_method, &pure_methods_closure, ignore_methods_with_assumenosideeffects, [&](DexMethod* other_method) { if (other_method != method) { lads.dependencies.insert(other_method); } return true; })) { unknown = true; } } break; } return unknown ? editable_cfg_adapter::LOOP_BREAK : editable_cfg_adapter::LOOP_CONTINUE; }); if (unknown) { return boost::none; } return lads; }, result, cache_config); } size_t compute_conditionally_pure_methods( const Scope& scope, const method_override_graph::Graph* method_override_graph, const method::ClInitHasNoSideEffectsPredicate& clinit_has_no_side_effects, const std::unordered_set<DexMethodRef*>& pure_methods, std::unordered_map<const DexMethod*, CseUnorderedLocationSet>* result, const purity::CacheConfig& cache_config) { Timer t("compute_conditionally_pure_methods"); auto iterations = analyze_read_locations( scope, method_override_graph, clinit_has_no_side_effects, pure_methods, /* ignore_methods_with_assumenosideeffects */ false, /* for_conditional_purity */ true, /* compute_locations */ true, result, cache_config); for (auto& p : *result) { TRACE(CSE, 4, "[CSE] conditionally pure method %s: %s", SHOW(p.first), SHOW(&p.second)); } return iterations; } size_t compute_no_side_effects_methods( const Scope& scope, const method_override_graph::Graph* method_override_graph, const method::ClInitHasNoSideEffectsPredicate& clinit_has_no_side_effects, const std::unordered_set<DexMethodRef*>& pure_methods, std::unordered_set<const DexMethod*>* result, const purity::CacheConfig& cache_config) { Timer t("compute_no_side_effects_methods"); std::unordered_map<const DexMethod*, CseUnorderedLocationSet> method_locations; auto iterations = analyze_read_locations( scope, method_override_graph, clinit_has_no_side_effects, pure_methods, /* ignore_methods_with_assumenosideeffects */ true, /* for_conditional_purity */ false, /* compute_locations */ false, &method_locations, cache_config); for (auto& p : method_locations) { TRACE(CSE, 4, "[CSE] no side effects method %s", SHOW(p.first)); result->insert(p.first); } return iterations; } bool has_implementor(const method_override_graph::Graph* method_override_graph, const DexMethod* method) { // For methods of an annotation interface, a synthetic trivial implementation // is generated by the runtime. if (is_annotation(type_class(method->get_class()))) { return true; } bool found_implementor = false; if (!process_base_and_overriding_methods( method_override_graph, method, /* methods_to_ignore */ nullptr, /* ignore_methods_with_assumenosideeffects */ false, [&](DexMethod*) { found_implementor = true; return true; })) { return true; } return found_implementor; }