diff --git a/source/fuzz/call_graph.cpp b/source/fuzz/call_graph.cpp index 15416fe3..c52bc342 100644 --- a/source/fuzz/call_graph.cpp +++ b/source/fuzz/call_graph.cpp @@ -20,12 +20,32 @@ namespace spvtools { namespace fuzz { CallGraph::CallGraph(opt::IRContext* context) { - // Initialize function in-degree and call graph edges to 0 and empty. + // Initialize function in-degree, call graph edges and corresponding maximum + // loop nesting depth to 0, empty and 0 respectively. for (auto& function : *context->module()) { function_in_degree_[function.result_id()] = 0; call_graph_edges_[function.result_id()] = std::set(); + function_max_loop_nesting_depth_[function.result_id()] = 0; } + // Record the maximum loop nesting depth for each edge, by keeping a map from + // pairs of function ids, where (A, B) represents a function call from A to B, + // to the corresponding maximum depth. + std::map, uint32_t> call_to_max_depth; + + // Compute |function_in_degree_|, |call_graph_edges_| and |call_to_max_depth|. + BuildGraphAndGetDepthOfFunctionCalls(context, &call_to_max_depth); + + // Compute |functions_in_topological_order_|. + ComputeTopologicalOrderOfFunctions(); + + // Compute |function_max_loop_nesting_depth_|. + ComputeInterproceduralFunctionCallDepths(call_to_max_depth); +} + +void CallGraph::BuildGraphAndGetDepthOfFunctionCalls( + opt::IRContext* context, + std::map, uint32_t>* call_to_max_depth) { // Consider every function. for (auto& function : *context->module()) { // Avoid considering the same callee of this function multiple times by @@ -39,6 +59,25 @@ CallGraph::CallGraph(opt::IRContext* context) { } // Get the id of the function being called. uint32_t callee = instruction.GetSingleWordInOperand(0); + + // Get the loop nesting depth of this function call. + uint32_t loop_nesting_depth = + context->GetStructuredCFGAnalysis()->LoopNestingDepth(block.id()); + // If inside a loop header, consider the function call nested inside the + // loop headed by the block. + if (block.IsLoopHeader()) { + loop_nesting_depth++; + } + + // Update the map if we have not seen this pair (caller, callee) + // before or if this function call is from a greater depth. + if (!known_callees.count(callee) || + call_to_max_depth->at({function.result_id(), callee}) < + loop_nesting_depth) { + call_to_max_depth->insert( + {{function.result_id(), callee}, loop_nesting_depth}); + } + if (known_callees.count(callee)) { // We have already considered a call to this function - ignore it. continue; @@ -53,6 +92,69 @@ CallGraph::CallGraph(opt::IRContext* context) { } } +void CallGraph::ComputeTopologicalOrderOfFunctions() { + // This is an implementation of Kahn’s algorithm for topological sorting. + + // Initialise |functions_in_topological_order_|. + functions_in_topological_order_.clear(); + + // Get a copy of the initial in-degrees of all functions. The algorithm + // involves decrementing these values, hence why we work on a copy. + std::map function_in_degree = GetFunctionInDegree(); + + // Populate a queue with all those function ids with in-degree zero. + std::queue queue; + for (auto& entry : function_in_degree) { + if (entry.second == 0) { + queue.push(entry.first); + } + } + + // Pop ids from the queue, adding them to the sorted order and decreasing the + // in-degrees of their successors. A successor who's in-degree becomes zero + // gets added to the queue. + while (!queue.empty()) { + auto next = queue.front(); + queue.pop(); + functions_in_topological_order_.push_back(next); + for (auto successor : GetDirectCallees(next)) { + assert(function_in_degree.at(successor) > 0 && + "The in-degree cannot be zero if the function is a successor."); + function_in_degree[successor] = function_in_degree.at(successor) - 1; + if (function_in_degree.at(successor) == 0) { + queue.push(successor); + } + } + } + + assert(functions_in_topological_order_.size() == function_in_degree.size() && + "Every function should appear in the sort."); + + return; +} + +void CallGraph::ComputeInterproceduralFunctionCallDepths( + const std::map, uint32_t>& + call_to_max_depth) { + // Find the maximum loop nesting depth that each function can be + // called from, by considering them in topological order. + for (uint32_t function_id : functions_in_topological_order_) { + const auto& callees = call_graph_edges_[function_id]; + + // For each callee, update its maximum loop nesting depth, if a call from + // |function_id| increases it. + for (uint32_t callee : callees) { + uint32_t max_depth_from_this_function = + function_max_loop_nesting_depth_[function_id] + + call_to_max_depth.at({function_id, callee}); + if (function_max_loop_nesting_depth_[callee] < + max_depth_from_this_function) { + function_max_loop_nesting_depth_[callee] = max_depth_from_this_function; + } + } + } +} + void CallGraph::PushDirectCallees(uint32_t function_id, std::queue* queue) const { for (auto callee : GetDirectCallees(function_id)) { diff --git a/source/fuzz/call_graph.h b/source/fuzz/call_graph.h index 14cd23b4..840b1f12 100644 --- a/source/fuzz/call_graph.h +++ b/source/fuzz/call_graph.h @@ -24,6 +24,9 @@ namespace spvtools { namespace fuzz { // Represents the acyclic call graph of a SPIR-V module. +// The module is assumed to be recursion-free, so there are no cycles in the +// graph. This class is immutable, so it will need to be recomputed if the +// module changes. class CallGraph { public: // Creates a call graph corresponding to the given SPIR-V module. @@ -43,7 +46,44 @@ class CallGraph { // invokes. std::set GetIndirectCallees(uint32_t function_id) const; + // Returns the ids of all the functions in the graph in a topological order, + // in relation to the function calls, which are assumed to be recursion-free. + const std::vector& GetFunctionsInTopologicalOrder() const { + return functions_in_topological_order_; + } + + // Returns the maximum loop nesting depth from which |function_id| can be + // called. This is computed inter-procedurally (i.e. if main calls A from + // depth 2 and A calls B from depth 1, the result will be 3 for A). + // This is a static analysis, so it's not necessarily true that the depth + // returned can actually be reached at runtime. + uint32_t GetMaxCallNestingDepth(uint32_t function_id) const { + return function_max_loop_nesting_depth_.at(function_id); + } + private: + // Computes |call_graph_edges_| and |function_in_degree_|. For each pair (A, + // B) of functions such that there is at least a function call from A to B, + // adds, to |call_to_max_depth|, a mapping from (A, B) to the maximum loop + // nesting depth (within A) of any such function call. + void BuildGraphAndGetDepthOfFunctionCalls( + opt::IRContext* context, + std::map, uint32_t>* call_to_max_depth); + + // Computes a topological order of the functions in the graph, writing the + // result to |functions_in_topological_order_|. Assumes that the function + // calls are recursion-free and that |function_in_degree_| has been computed. + void ComputeTopologicalOrderOfFunctions(); + + // Computes |function_max_loop_nesting_depth_| so that each function is mapped + // to the maximum loop nesting depth from which it can be called, as described + // by the comment to GetMaxCallNestingDepth. Assumes that |call_graph_edges_| + // and |functions_in_topological_order_| have been computed, and that + // |call_to_max_depth| contains a mapping for each edge in the graph. + void ComputeInterproceduralFunctionCallDepths( + const std::map, uint32_t>& + call_to_max_depth); + // Pushes the direct callees of |function_id| on to |queue|. void PushDirectCallees(uint32_t function_id, std::queue* queue) const; @@ -54,6 +94,14 @@ class CallGraph { // For each function id, stores the number of distinct functions that call // the function. std::map function_in_degree_; + + // Stores the ids of the functions in a topological order, + // in relation to the function calls, which are assumed to be recursion-free. + std::vector functions_in_topological_order_; + + // For each function id, stores the maximum loop nesting depth that the + // function can be called from. + std::map function_max_loop_nesting_depth_; }; } // namespace fuzz diff --git a/source/fuzz/fuzzer_pass_donate_modules.cpp b/source/fuzz/fuzzer_pass_donate_modules.cpp index f54d3e4e..afa22247 100644 --- a/source/fuzz/fuzzer_pass_donate_modules.cpp +++ b/source/fuzz/fuzzer_pass_donate_modules.cpp @@ -598,7 +598,7 @@ void FuzzerPassDonateModules::HandleFunctions( // Get the ids of functions in the donor module, topologically sorted // according to the donor's call graph. auto topological_order = - GetFunctionsInCallGraphTopologicalOrder(donor_ir_context); + CallGraph(donor_ir_context).GetFunctionsInTopologicalOrder(); // Donate the functions in reverse topological order. This ensures that a // function gets donated before any function that depends on it. This allows @@ -796,52 +796,6 @@ bool FuzzerPassDonateModules::IsBasicType( } } -std::vector -FuzzerPassDonateModules::GetFunctionsInCallGraphTopologicalOrder( - opt::IRContext* context) { - CallGraph call_graph(context); - - // This is an implementation of Kahn’s algorithm for topological sorting. - - // This is the sorted order of function ids that we will eventually return. - std::vector result; - - // Get a copy of the initial in-degrees of all functions. The algorithm - // involves decrementing these values, hence why we work on a copy. - std::map function_in_degree = - call_graph.GetFunctionInDegree(); - - // Populate a queue with all those function ids with in-degree zero. - std::queue queue; - for (auto& entry : function_in_degree) { - if (entry.second == 0) { - queue.push(entry.first); - } - } - - // Pop ids from the queue, adding them to the sorted order and decreasing the - // in-degrees of their successors. A successor who's in-degree becomes zero - // gets added to the queue. - while (!queue.empty()) { - auto next = queue.front(); - queue.pop(); - result.push_back(next); - for (auto successor : call_graph.GetDirectCallees(next)) { - assert(function_in_degree.at(successor) > 0 && - "The in-degree cannot be zero if the function is a successor."); - function_in_degree[successor] = function_in_degree.at(successor) - 1; - if (function_in_degree.at(successor) == 0) { - queue.push(successor); - } - } - } - - assert(result.size() == function_in_degree.size() && - "Every function should appear in the sort."); - - return result; -} - void FuzzerPassDonateModules::HandleOpArrayLength( const opt::Instruction& instruction, std::map* original_id_to_donated_id, diff --git a/source/fuzz/fuzzer_pass_donate_modules.h b/source/fuzz/fuzzer_pass_donate_modules.h index 89858e44..0424cece 100644 --- a/source/fuzz/fuzzer_pass_donate_modules.h +++ b/source/fuzz/fuzzer_pass_donate_modules.h @@ -154,12 +154,6 @@ class FuzzerPassDonateModules : public FuzzerPass { // array or struct; i.e. it is not an opaque type. bool IsBasicType(const opt::Instruction& instruction) const; - // Returns the ids of all functions in |context| in a topological order in - // relation to the call graph of |context|, which is assumed to be recursion- - // free. - static std::vector GetFunctionsInCallGraphTopologicalOrder( - opt::IRContext* context); - // Functions that supply SPIR-V modules std::vector donor_suppliers_; }; diff --git a/test/fuzz/CMakeLists.txt b/test/fuzz/CMakeLists.txt index 1322dad0..6a6b472b 100644 --- a/test/fuzz/CMakeLists.txt +++ b/test/fuzz/CMakeLists.txt @@ -17,6 +17,7 @@ if (${SPIRV_BUILD_FUZZER}) set(SOURCES fuzz_test_util.h + call_graph_test.cpp data_synonym_transformation_test.cpp equivalence_relation_test.cpp fact_manager_test.cpp diff --git a/test/fuzz/call_graph_test.cpp b/test/fuzz/call_graph_test.cpp new file mode 100644 index 00000000..d8b31349 --- /dev/null +++ b/test/fuzz/call_graph_test.cpp @@ -0,0 +1,365 @@ +// Copyright (c) 2020 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include "source/fuzz/call_graph.h" +#include "test/fuzz/fuzz_test_util.h" + +namespace spvtools { +namespace fuzz { +namespace { + +// The SPIR-V came from this GLSL, slightly modified +// (main is %2, A is %35, B is %48, C is %50, D is %61): +// +// #version 310 es +// +// int A (int x) { +// return x + 1; +// } +// +// void D() { +// } +// +// void C() { +// int x = 0; +// int y = 0; +// +// while (x < 10) { +// while (y < 10) { +// y = A(y); +// } +// x = A(x); +// } +// } +// +// void B () { +// int x = 0; +// int y = 0; +// +// while (x < 10) { +// D(); +// while (y < 10) { +// y = A(y); +// C(); +// } +// x++; +// } +// +// } +// +// void main() +// { +// int x = 0; +// int y = 0; +// int z = 0; +// +// while (x < 10) { +// while(y < 10) { +// y = A(x); +// while (z < 10) { +// z = A(z); +// } +// } +// x += 2; +// } +// +// B(); +// C(); +// } +std::string shader = R"( + OpCapability Shader + %1 = OpExtInstImport "GLSL.std.450" + OpMemoryModel Logical GLSL450 + OpEntryPoint Fragment %2 "main" + OpExecutionMode %2 OriginUpperLeft + OpSource ESSL 310 + %3 = OpTypeVoid + %4 = OpTypeFunction %3 + %5 = OpTypeInt 32 1 + %6 = OpTypePointer Function %5 + %7 = OpTypeFunction %5 %6 + %8 = OpConstant %5 1 + %9 = OpConstant %5 0 + %10 = OpConstant %5 10 + %11 = OpTypeBool + %12 = OpConstant %5 2 + %2 = OpFunction %3 None %4 + %13 = OpLabel + %14 = OpVariable %6 Function + %15 = OpVariable %6 Function + %16 = OpVariable %6 Function + %17 = OpVariable %6 Function + %18 = OpVariable %6 Function + OpStore %14 %9 + OpStore %15 %9 + OpStore %16 %9 + OpBranch %19 + %19 = OpLabel + OpLoopMerge %20 %21 None + OpBranch %22 + %22 = OpLabel + %23 = OpLoad %5 %14 + %24 = OpSLessThan %11 %23 %10 + OpBranchConditional %24 %25 %20 + %25 = OpLabel + OpBranch %26 + %26 = OpLabel + OpLoopMerge %27 %28 None + OpBranch %29 + %29 = OpLabel + %30 = OpLoad %5 %15 + %31 = OpSLessThan %11 %30 %10 + OpBranchConditional %31 %32 %27 + %32 = OpLabel + %33 = OpLoad %5 %14 + OpStore %17 %33 + %34 = OpFunctionCall %5 %35 %17 + OpStore %15 %34 + OpBranch %36 + %36 = OpLabel + OpLoopMerge %37 %38 None + OpBranch %39 + %39 = OpLabel + %40 = OpLoad %5 %16 + %41 = OpSLessThan %11 %40 %10 + OpBranchConditional %41 %42 %37 + %42 = OpLabel + %43 = OpLoad %5 %16 + OpStore %18 %43 + %44 = OpFunctionCall %5 %35 %18 + OpStore %16 %44 + OpBranch %38 + %38 = OpLabel + OpBranch %36 + %37 = OpLabel + OpBranch %28 + %28 = OpLabel + OpBranch %26 + %27 = OpLabel + %45 = OpLoad %5 %14 + %46 = OpIAdd %5 %45 %12 + OpStore %14 %46 + OpBranch %21 + %21 = OpLabel + OpBranch %19 + %20 = OpLabel + %47 = OpFunctionCall %3 %48 + %49 = OpFunctionCall %3 %50 + OpReturn + OpFunctionEnd + %35 = OpFunction %5 None %7 + %51 = OpFunctionParameter %6 + %52 = OpLabel + %53 = OpLoad %5 %51 + %54 = OpIAdd %5 %53 %8 + OpReturnValue %54 + OpFunctionEnd + %48 = OpFunction %3 None %4 + %55 = OpLabel + %56 = OpVariable %6 Function + %57 = OpVariable %6 Function + %58 = OpVariable %6 Function + OpStore %56 %9 + OpStore %57 %9 + OpBranch %59 + %59 = OpLabel + %60 = OpFunctionCall %3 %61 + OpLoopMerge %62 %63 None + OpBranch %64 + %64 = OpLabel + OpLoopMerge %65 %66 None + OpBranch %67 + %67 = OpLabel + %68 = OpLoad %5 %57 + %69 = OpSLessThan %11 %68 %10 + OpBranchConditional %69 %70 %65 + %70 = OpLabel + %71 = OpLoad %5 %57 + OpStore %58 %71 + %72 = OpFunctionCall %5 %35 %58 + OpStore %57 %72 + %73 = OpFunctionCall %3 %50 + OpBranch %66 + %66 = OpLabel + OpBranch %64 + %65 = OpLabel + %74 = OpLoad %5 %56 + %75 = OpIAdd %5 %74 %8 + OpStore %56 %75 + OpBranch %63 + %63 = OpLabel + %76 = OpLoad %5 %56 + %77 = OpSLessThan %11 %76 %10 + OpBranchConditional %77 %59 %62 + %62 = OpLabel + OpReturn + OpFunctionEnd + %50 = OpFunction %3 None %4 + %78 = OpLabel + %79 = OpVariable %6 Function + %80 = OpVariable %6 Function + %81 = OpVariable %6 Function + %82 = OpVariable %6 Function + OpStore %79 %9 + OpStore %80 %9 + OpBranch %83 + %83 = OpLabel + OpLoopMerge %84 %85 None + OpBranch %86 + %86 = OpLabel + %87 = OpLoad %5 %79 + %88 = OpSLessThan %11 %87 %10 + OpBranchConditional %88 %89 %84 + %89 = OpLabel + OpBranch %90 + %90 = OpLabel + OpLoopMerge %91 %92 None + OpBranch %93 + %93 = OpLabel + %94 = OpLoad %5 %80 + %95 = OpSLessThan %11 %94 %10 + OpBranchConditional %95 %96 %91 + %96 = OpLabel + %97 = OpLoad %5 %80 + OpStore %81 %97 + %98 = OpFunctionCall %5 %35 %81 + OpStore %80 %98 + OpBranch %92 + %92 = OpLabel + OpBranch %90 + %91 = OpLabel + %99 = OpLoad %5 %79 + OpStore %82 %99 + %100 = OpFunctionCall %5 %35 %82 + OpStore %79 %100 + OpBranch %85 + %85 = OpLabel + OpBranch %83 + %84 = OpLabel + OpReturn + OpFunctionEnd + %61 = OpFunction %3 None %4 + %101 = OpLabel + OpReturn + OpFunctionEnd +)"; + +// We have that: +// main calls: +// - A (maximum loop nesting depth of function call: 3) +// - B (0) +// - C (0) +// A calls nothing. +// B calls: +// - A (2) +// - C (2) +// - D (1) +// C calls: +// - A (2) +// D calls nothing. + +TEST(CallGraphTest, FunctionInDegree) { + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + const auto graph = CallGraph(context.get()); + + const auto& function_in_degree = graph.GetFunctionInDegree(); + // Check the in-degrees of, in order: main, A, B, C, D. + ASSERT_EQ(function_in_degree.at(2), 0); + ASSERT_EQ(function_in_degree.at(35), 3); + ASSERT_EQ(function_in_degree.at(48), 1); + ASSERT_EQ(function_in_degree.at(50), 2); + ASSERT_EQ(function_in_degree.at(61), 1); +} + +TEST(CallGraphTest, DirectCallees) { + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + const auto graph = CallGraph(context.get()); + + // Check the callee sets of, in order: main, A, B, C, D. + ASSERT_EQ(graph.GetDirectCallees(2), std::set({35, 48, 50})); + ASSERT_EQ(graph.GetDirectCallees(35), std::set({})); + ASSERT_EQ(graph.GetDirectCallees(48), std::set({35, 50, 61})); + ASSERT_EQ(graph.GetDirectCallees(50), std::set({35})); + ASSERT_EQ(graph.GetDirectCallees(61), std::set({})); +} + +TEST(CallGraphTest, IndirectCallees) { + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + const auto graph = CallGraph(context.get()); + + // Check the callee sets of, in order: main, A, B, C, D. + ASSERT_EQ(graph.GetIndirectCallees(2), std::set({35, 48, 50, 61})); + ASSERT_EQ(graph.GetDirectCallees(35), std::set({})); + ASSERT_EQ(graph.GetDirectCallees(48), std::set({35, 50, 61})); + ASSERT_EQ(graph.GetDirectCallees(50), std::set({35})); + ASSERT_EQ(graph.GetDirectCallees(61), std::set({})); +} + +TEST(CallGraphTest, TopologicalOrder) { + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + const auto graph = CallGraph(context.get()); + + const auto& topological_ordering = graph.GetFunctionsInTopologicalOrder(); + + // The possible topological orderings are: + // - main, B, D, C, A + // - main, B, C, D, A + // - main, B, C, A, D + ASSERT_TRUE( + topological_ordering == std::vector({2, 48, 61, 50, 35}) || + topological_ordering == std::vector({2, 48, 50, 61, 35}) || + topological_ordering == std::vector({2, 48, 50, 35, 61})); +} + +TEST(CallGraphTest, LoopNestingDepth) { + const auto env = SPV_ENV_UNIVERSAL_1_5; + const auto consumer = nullptr; + + const auto context = BuildModule(env, consumer, shader, kFuzzAssembleOption); + ASSERT_TRUE(IsValid(env, context.get())); + + const auto graph = CallGraph(context.get()); + + // Check the maximum loop nesting depth for function calls to, in order: + // main, A, B, C, D + ASSERT_EQ(graph.GetMaxCallNestingDepth(2), 0); + ASSERT_EQ(graph.GetMaxCallNestingDepth(35), 4); + ASSERT_EQ(graph.GetMaxCallNestingDepth(48), 0); + ASSERT_EQ(graph.GetMaxCallNestingDepth(50), 2); + ASSERT_EQ(graph.GetMaxCallNestingDepth(61), 1); +} + +} // namespace +} // namespace fuzz +} // namespace spvtools