blob: 95838380cceba5c23ce375987325e4b5ed2fffb1 [file] [log] [blame]
Aart Bik281c6812016-08-26 11:31:48 -07001/*
2 * Copyright (C) 2016 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 * http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17#include "loop_optimization.h"
18
Aart Bik96202302016-10-04 17:33:56 -070019#include "linear_order.h"
Aart Bik281c6812016-08-26 11:31:48 -070020
21namespace art {
22
Aart Bik9abf8942016-10-14 09:49:42 -070023// Remove the instruction from the graph. A bit more elaborate than the usual
24// instruction removal, since there may be a cycle in the use structure.
Aart Bik281c6812016-08-26 11:31:48 -070025static void RemoveFromCycle(HInstruction* instruction) {
Aart Bik281c6812016-08-26 11:31:48 -070026 instruction->RemoveAsUserOfAllInputs();
27 instruction->RemoveEnvironmentUsers();
28 instruction->GetBlock()->RemoveInstructionOrPhi(instruction, /*ensure_safety=*/ false);
29}
30
Aart Bik807868e2016-11-03 17:51:43 -070031// Detect a goto block and sets succ to the single successor.
Aart Bike3dedc52016-11-02 17:50:27 -070032static bool IsGotoBlock(HBasicBlock* block, /*out*/ HBasicBlock** succ) {
33 if (block->GetPredecessors().size() == 1 &&
34 block->GetSuccessors().size() == 1 &&
35 block->IsSingleGoto()) {
36 *succ = block->GetSingleSuccessor();
37 return true;
38 }
39 return false;
40}
41
Aart Bik807868e2016-11-03 17:51:43 -070042// Detect an early exit loop.
43static bool IsEarlyExit(HLoopInformation* loop_info) {
44 HBlocksInLoopReversePostOrderIterator it_loop(*loop_info);
45 for (it_loop.Advance(); !it_loop.Done(); it_loop.Advance()) {
46 for (HBasicBlock* successor : it_loop.Current()->GetSuccessors()) {
47 if (!loop_info->Contains(*successor)) {
48 return true;
49 }
50 }
51 }
52 return false;
53}
54
Aart Bik281c6812016-08-26 11:31:48 -070055//
56// Class methods.
57//
58
59HLoopOptimization::HLoopOptimization(HGraph* graph,
60 HInductionVarAnalysis* induction_analysis)
61 : HOptimization(graph, kLoopOptimizationPassName),
62 induction_range_(induction_analysis),
Aart Bik96202302016-10-04 17:33:56 -070063 loop_allocator_(nullptr),
Aart Bik281c6812016-08-26 11:31:48 -070064 top_loop_(nullptr),
Aart Bik8c4a8542016-10-06 11:36:57 -070065 last_loop_(nullptr),
Aart Bik482095d2016-10-10 15:39:10 -070066 iset_(nullptr),
Aart Bikdf7822e2016-12-06 10:05:30 -080067 induction_simplication_count_(0),
68 simplified_(false) {
Aart Bik281c6812016-08-26 11:31:48 -070069}
70
71void HLoopOptimization::Run() {
72 // Well-behaved loops only.
73 // TODO: make this less of a sledgehammer.
Aart Bik96202302016-10-04 17:33:56 -070074 if (graph_->HasTryCatch() || graph_->HasIrreducibleLoops()) {
Aart Bik281c6812016-08-26 11:31:48 -070075 return;
76 }
77
Aart Bik96202302016-10-04 17:33:56 -070078 // Phase-local allocator that draws from the global pool. Since the allocator
79 // itself resides on the stack, it is destructed on exiting Run(), which
80 // implies its underlying memory is released immediately.
Nicolas Geoffrayebe16742016-10-05 09:55:42 +010081 ArenaAllocator allocator(graph_->GetArena()->GetArenaPool());
Aart Bik96202302016-10-04 17:33:56 -070082 loop_allocator_ = &allocator;
Nicolas Geoffrayebe16742016-10-05 09:55:42 +010083
Aart Bik96202302016-10-04 17:33:56 -070084 // Perform loop optimizations.
85 LocalRun();
86
87 // Detach.
88 loop_allocator_ = nullptr;
89 last_loop_ = top_loop_ = nullptr;
90}
91
92void HLoopOptimization::LocalRun() {
93 // Build the linear order using the phase-local allocator. This step enables building
94 // a loop hierarchy that properly reflects the outer-inner and previous-next relation.
95 ArenaVector<HBasicBlock*> linear_order(loop_allocator_->Adapter(kArenaAllocLinearOrder));
96 LinearizeGraph(graph_, loop_allocator_, &linear_order);
97
Aart Bik281c6812016-08-26 11:31:48 -070098 // Build the loop hierarchy.
Aart Bik96202302016-10-04 17:33:56 -070099 for (HBasicBlock* block : linear_order) {
Aart Bik281c6812016-08-26 11:31:48 -0700100 if (block->IsLoopHeader()) {
101 AddLoop(block->GetLoopInformation());
102 }
103 }
Aart Bik96202302016-10-04 17:33:56 -0700104
Aart Bik8c4a8542016-10-06 11:36:57 -0700105 // Traverse the loop hierarchy inner-to-outer and optimize. Traversal can use
106 // a temporary set that stores instructions using the phase-local allocator.
107 if (top_loop_ != nullptr) {
108 ArenaSet<HInstruction*> iset(loop_allocator_->Adapter(kArenaAllocLoopOptimization));
109 iset_ = &iset;
110 TraverseLoopsInnerToOuter(top_loop_);
111 iset_ = nullptr; // detach
112 }
Aart Bik281c6812016-08-26 11:31:48 -0700113}
114
115void HLoopOptimization::AddLoop(HLoopInformation* loop_info) {
116 DCHECK(loop_info != nullptr);
Nicolas Geoffrayebe16742016-10-05 09:55:42 +0100117 LoopNode* node = new (loop_allocator_) LoopNode(loop_info); // phase-local allocator
Aart Bik281c6812016-08-26 11:31:48 -0700118 if (last_loop_ == nullptr) {
119 // First loop.
120 DCHECK(top_loop_ == nullptr);
121 last_loop_ = top_loop_ = node;
122 } else if (loop_info->IsIn(*last_loop_->loop_info)) {
123 // Inner loop.
124 node->outer = last_loop_;
125 DCHECK(last_loop_->inner == nullptr);
126 last_loop_ = last_loop_->inner = node;
127 } else {
128 // Subsequent loop.
129 while (last_loop_->outer != nullptr && !loop_info->IsIn(*last_loop_->outer->loop_info)) {
130 last_loop_ = last_loop_->outer;
131 }
132 node->outer = last_loop_->outer;
133 node->previous = last_loop_;
134 DCHECK(last_loop_->next == nullptr);
135 last_loop_ = last_loop_->next = node;
136 }
137}
138
139void HLoopOptimization::RemoveLoop(LoopNode* node) {
140 DCHECK(node != nullptr);
Aart Bik8c4a8542016-10-06 11:36:57 -0700141 DCHECK(node->inner == nullptr);
142 if (node->previous != nullptr) {
143 // Within sequence.
144 node->previous->next = node->next;
145 if (node->next != nullptr) {
146 node->next->previous = node->previous;
147 }
148 } else {
149 // First of sequence.
150 if (node->outer != nullptr) {
151 node->outer->inner = node->next;
152 } else {
153 top_loop_ = node->next;
154 }
155 if (node->next != nullptr) {
156 node->next->outer = node->outer;
157 node->next->previous = nullptr;
158 }
159 }
Aart Bik281c6812016-08-26 11:31:48 -0700160}
161
162void HLoopOptimization::TraverseLoopsInnerToOuter(LoopNode* node) {
163 for ( ; node != nullptr; node = node->next) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800164 // Visit inner loops first.
Aart Bik482095d2016-10-10 15:39:10 -0700165 int current_induction_simplification_count = induction_simplication_count_;
Aart Bik281c6812016-08-26 11:31:48 -0700166 if (node->inner != nullptr) {
167 TraverseLoopsInnerToOuter(node->inner);
168 }
Aart Bik6b69e0a2017-01-11 10:20:43 -0800169 // Recompute induction information of this loop if the induction
170 // of any inner loop has been simplified.
Aart Bik482095d2016-10-10 15:39:10 -0700171 if (current_induction_simplification_count != induction_simplication_count_) {
172 induction_range_.ReVisit(node->loop_info);
173 }
Aart Bik6b69e0a2017-01-11 10:20:43 -0800174 // Repeat simplifications in the body of this loop until no more changes occur.
175 // Note that since each simplification consists of eliminating code (without
176 // introducing new code), this process is always finite.
Aart Bikdf7822e2016-12-06 10:05:30 -0800177 do {
178 simplified_ = false;
Aart Bikdf7822e2016-12-06 10:05:30 -0800179 SimplifyInduction(node);
Aart Bik6b69e0a2017-01-11 10:20:43 -0800180 SimplifyBlocks(node);
Aart Bikdf7822e2016-12-06 10:05:30 -0800181 } while (simplified_);
Aart Bik6b69e0a2017-01-11 10:20:43 -0800182 // Simplify inner loop.
Aart Bik9abf8942016-10-14 09:49:42 -0700183 if (node->inner == nullptr) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800184 SimplifyInnerLoop(node);
Aart Bik9abf8942016-10-14 09:49:42 -0700185 }
Aart Bik281c6812016-08-26 11:31:48 -0700186 }
187}
188
189void HLoopOptimization::SimplifyInduction(LoopNode* node) {
190 HBasicBlock* header = node->loop_info->GetHeader();
191 HBasicBlock* preheader = node->loop_info->GetPreHeader();
Aart Bik8c4a8542016-10-06 11:36:57 -0700192 // Scan the phis in the header to find opportunities to simplify an induction
193 // cycle that is only used outside the loop. Replace these uses, if any, with
194 // the last value and remove the induction cycle.
195 // Examples: for (int i = 0; x != null; i++) { .... no i .... }
196 // for (int i = 0; i < 10; i++, k++) { .... no k .... } return k;
Aart Bik281c6812016-08-26 11:31:48 -0700197 for (HInstructionIterator it(header->GetPhis()); !it.Done(); it.Advance()) {
198 HPhi* phi = it.Current()->AsPhi();
Aart Bik8c4a8542016-10-06 11:36:57 -0700199 iset_->clear();
200 int32_t use_count = 0;
Aart Bikcc42be02016-10-20 16:14:16 -0700201 if (IsPhiInduction(phi) &&
Aart Bik6b69e0a2017-01-11 10:20:43 -0800202 IsOnlyUsedAfterLoop(node->loop_info, phi, /*collect_loop_uses*/ false, &use_count) &&
Aart Bik807868e2016-11-03 17:51:43 -0700203 // No uses, or no early-exit with proper replacement.
204 (use_count == 0 ||
205 (!IsEarlyExit(node->loop_info) && TryReplaceWithLastValue(phi, preheader)))) {
Aart Bik8c4a8542016-10-06 11:36:57 -0700206 for (HInstruction* i : *iset_) {
207 RemoveFromCycle(i);
Aart Bik281c6812016-08-26 11:31:48 -0700208 }
Aart Bikdf7822e2016-12-06 10:05:30 -0800209 simplified_ = true;
Aart Bik482095d2016-10-10 15:39:10 -0700210 }
211 }
212}
213
214void HLoopOptimization::SimplifyBlocks(LoopNode* node) {
Aart Bikdf7822e2016-12-06 10:05:30 -0800215 // Iterate over all basic blocks in the loop-body.
216 for (HBlocksInLoopIterator it(*node->loop_info); !it.Done(); it.Advance()) {
217 HBasicBlock* block = it.Current();
218 // Remove dead instructions from the loop-body.
Aart Bik6b69e0a2017-01-11 10:20:43 -0800219 RemoveDeadInstructions(block->GetPhis());
220 RemoveDeadInstructions(block->GetInstructions());
Aart Bikdf7822e2016-12-06 10:05:30 -0800221 // Remove trivial control flow blocks from the loop-body.
Aart Bik6b69e0a2017-01-11 10:20:43 -0800222 if (block->GetPredecessors().size() == 1 &&
223 block->GetSuccessors().size() == 1 &&
224 block->GetSingleSuccessor()->GetPredecessors().size() == 1) {
Aart Bikdf7822e2016-12-06 10:05:30 -0800225 simplified_ = true;
Aart Bik6b69e0a2017-01-11 10:20:43 -0800226 block->MergeWith(block->GetSingleSuccessor());
Aart Bikdf7822e2016-12-06 10:05:30 -0800227 } else if (block->GetSuccessors().size() == 2) {
228 // Trivial if block can be bypassed to either branch.
229 HBasicBlock* succ0 = block->GetSuccessors()[0];
230 HBasicBlock* succ1 = block->GetSuccessors()[1];
231 HBasicBlock* meet0 = nullptr;
232 HBasicBlock* meet1 = nullptr;
233 if (succ0 != succ1 &&
234 IsGotoBlock(succ0, &meet0) &&
235 IsGotoBlock(succ1, &meet1) &&
236 meet0 == meet1 && // meets again
237 meet0 != block && // no self-loop
238 meet0->GetPhis().IsEmpty()) { // not used for merging
239 simplified_ = true;
240 succ0->DisconnectAndDelete();
241 if (block->Dominates(meet0)) {
242 block->RemoveDominatedBlock(meet0);
243 succ1->AddDominatedBlock(meet0);
244 meet0->SetDominator(succ1);
Aart Bike3dedc52016-11-02 17:50:27 -0700245 }
Aart Bik482095d2016-10-10 15:39:10 -0700246 }
Aart Bik281c6812016-08-26 11:31:48 -0700247 }
Aart Bikdf7822e2016-12-06 10:05:30 -0800248 }
Aart Bik281c6812016-08-26 11:31:48 -0700249}
250
Aart Bik6b69e0a2017-01-11 10:20:43 -0800251bool HLoopOptimization::SimplifyInnerLoop(LoopNode* node) {
Aart Bik281c6812016-08-26 11:31:48 -0700252 HBasicBlock* header = node->loop_info->GetHeader();
253 HBasicBlock* preheader = node->loop_info->GetPreHeader();
Aart Bik9abf8942016-10-14 09:49:42 -0700254 // Ensure loop header logic is finite.
Aart Bik6b69e0a2017-01-11 10:20:43 -0800255 int64_t tc = 0;
256 if (!induction_range_.IsFinite(node->loop_info, &tc)) {
257 return false;
Aart Bik9abf8942016-10-14 09:49:42 -0700258 }
Aart Bik281c6812016-08-26 11:31:48 -0700259 // Ensure there is only a single loop-body (besides the header).
260 HBasicBlock* body = nullptr;
261 for (HBlocksInLoopIterator it(*node->loop_info); !it.Done(); it.Advance()) {
262 if (it.Current() != header) {
263 if (body != nullptr) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800264 return false;
Aart Bik281c6812016-08-26 11:31:48 -0700265 }
266 body = it.Current();
267 }
268 }
269 // Ensure there is only a single exit point.
270 if (header->GetSuccessors().size() != 2) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800271 return false;
Aart Bik281c6812016-08-26 11:31:48 -0700272 }
273 HBasicBlock* exit = (header->GetSuccessors()[0] == body)
274 ? header->GetSuccessors()[1]
275 : header->GetSuccessors()[0];
Aart Bik8c4a8542016-10-06 11:36:57 -0700276 // Ensure exit can only be reached by exiting loop.
Aart Bik281c6812016-08-26 11:31:48 -0700277 if (exit->GetPredecessors().size() != 1) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800278 return false;
Aart Bik281c6812016-08-26 11:31:48 -0700279 }
Aart Bik6b69e0a2017-01-11 10:20:43 -0800280 // Detect either an empty loop (no side effects other than plain iteration) or
281 // a trivial loop (just iterating once). Replace subsequent index uses, if any,
282 // with the last value and remove the loop, possibly after unrolling its body.
283 HInstruction* phi = header->GetFirstPhi();
Aart Bik8c4a8542016-10-06 11:36:57 -0700284 iset_->clear();
285 int32_t use_count = 0;
Aart Bik6b69e0a2017-01-11 10:20:43 -0800286 if (IsEmptyHeader(header)) {
287 bool is_empty = IsEmptyBody(body);
288 if ((is_empty || tc == 1) &&
289 IsOnlyUsedAfterLoop(node->loop_info, phi, /*collect_loop_uses*/ true, &use_count) &&
290 // No uses, or proper replacement.
291 (use_count == 0 || TryReplaceWithLastValue(phi, preheader))) {
292 if (!is_empty) {
293 // Unroll the loop body, which sees initial value of the index.
294 phi->ReplaceWith(phi->InputAt(0));
295 preheader->MergeInstructionsWith(body);
296 }
297 body->DisconnectAndDelete();
298 exit->RemovePredecessor(header);
299 header->RemoveSuccessor(exit);
300 header->RemoveDominatedBlock(exit);
301 header->DisconnectAndDelete();
302 preheader->AddSuccessor(exit);
303 preheader->AddInstruction(new (graph_->GetArena()) HGoto()); // global allocator
304 preheader->AddDominatedBlock(exit);
305 exit->SetDominator(preheader);
306 RemoveLoop(node); // update hierarchy
307 return true;
308 }
Aart Bik281c6812016-08-26 11:31:48 -0700309 }
Aart Bik6b69e0a2017-01-11 10:20:43 -0800310 return false;
Aart Bik281c6812016-08-26 11:31:48 -0700311}
312
Aart Bikcc42be02016-10-20 16:14:16 -0700313bool HLoopOptimization::IsPhiInduction(HPhi* phi) {
314 ArenaSet<HInstruction*>* set = induction_range_.LookupCycle(phi);
315 if (set != nullptr) {
Aart Bike3dedc52016-11-02 17:50:27 -0700316 DCHECK(iset_->empty());
Aart Bikcc42be02016-10-20 16:14:16 -0700317 for (HInstruction* i : *set) {
Aart Bike3dedc52016-11-02 17:50:27 -0700318 // Check that, other than instructions that are no longer in the graph (removed earlier)
319 // each instruction is removable and, other than the phi, uses are contained in the cycle.
320 if (!i->IsInBlock()) {
321 continue;
322 } else if (!i->IsRemovable()) {
323 return false;
324 } else if (i != phi) {
Aart Bikcc42be02016-10-20 16:14:16 -0700325 for (const HUseListNode<HInstruction*>& use : i->GetUses()) {
326 if (set->find(use.GetUser()) == set->end()) {
327 return false;
328 }
329 }
330 }
Aart Bike3dedc52016-11-02 17:50:27 -0700331 iset_->insert(i); // copy
Aart Bikcc42be02016-10-20 16:14:16 -0700332 }
Aart Bikcc42be02016-10-20 16:14:16 -0700333 return true;
334 }
335 return false;
336}
337
338// Find: phi: Phi(init, addsub)
339// s: SuspendCheck
340// c: Condition(phi, bound)
341// i: If(c)
342// TODO: Find a less pattern matching approach?
343bool HLoopOptimization::IsEmptyHeader(HBasicBlock* block) {
344 DCHECK(iset_->empty());
345 HInstruction* phi = block->GetFirstPhi();
346 if (phi != nullptr && phi->GetNext() == nullptr && IsPhiInduction(phi->AsPhi())) {
347 HInstruction* s = block->GetFirstInstruction();
348 if (s != nullptr && s->IsSuspendCheck()) {
349 HInstruction* c = s->GetNext();
350 if (c != nullptr && c->IsCondition() && c->GetUses().HasExactlyOneElement()) {
351 HInstruction* i = c->GetNext();
352 if (i != nullptr && i->IsIf() && i->InputAt(0) == c) {
353 iset_->insert(c);
354 iset_->insert(s);
355 return true;
356 }
357 }
358 }
359 }
360 return false;
361}
362
363bool HLoopOptimization::IsEmptyBody(HBasicBlock* block) {
364 if (block->GetFirstPhi() == nullptr) {
365 for (HInstructionIterator it(block->GetInstructions()); !it.Done(); it.Advance()) {
366 HInstruction* instruction = it.Current();
367 if (!instruction->IsGoto() && iset_->find(instruction) == iset_->end()) {
368 return false;
369 }
370 }
371 return true;
372 }
373 return false;
374}
375
Aart Bik482095d2016-10-10 15:39:10 -0700376bool HLoopOptimization::IsOnlyUsedAfterLoop(HLoopInformation* loop_info,
Aart Bik8c4a8542016-10-06 11:36:57 -0700377 HInstruction* instruction,
Aart Bik6b69e0a2017-01-11 10:20:43 -0800378 bool collect_loop_uses,
Aart Bik8c4a8542016-10-06 11:36:57 -0700379 /*out*/ int32_t* use_count) {
380 for (const HUseListNode<HInstruction*>& use : instruction->GetUses()) {
381 HInstruction* user = use.GetUser();
382 if (iset_->find(user) == iset_->end()) { // not excluded?
383 HLoopInformation* other_loop_info = user->GetBlock()->GetLoopInformation();
Aart Bik482095d2016-10-10 15:39:10 -0700384 if (other_loop_info != nullptr && other_loop_info->IsIn(*loop_info)) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800385 // If collect_loop_uses is set, simply keep adding those uses to the set.
386 // Otherwise, reject uses inside the loop that were not already in the set.
387 if (collect_loop_uses) {
388 iset_->insert(user);
389 continue;
390 }
Aart Bik8c4a8542016-10-06 11:36:57 -0700391 return false;
392 }
393 ++*use_count;
394 }
395 }
396 return true;
397}
398
Aart Bik807868e2016-11-03 17:51:43 -0700399bool HLoopOptimization::TryReplaceWithLastValue(HInstruction* instruction, HBasicBlock* block) {
400 // Try to replace outside uses with the last value. Environment uses can consume this
401 // value too, since any first true use is outside the loop (although this may imply
402 // that de-opting may look "ahead" a bit on the phi value). If there are only environment
403 // uses, the value is dropped altogether, since the computations have no effect.
404 if (induction_range_.CanGenerateLastValue(instruction)) {
Aart Bik6b69e0a2017-01-11 10:20:43 -0800405 HInstruction* replacement = induction_range_.GenerateLastValue(instruction, graph_, block);
406 const HUseList<HInstruction*>& uses = instruction->GetUses();
407 for (auto it = uses.begin(), end = uses.end(); it != end;) {
408 HInstruction* user = it->GetUser();
409 size_t index = it->GetIndex();
410 ++it; // increment before replacing
411 if (iset_->find(user) == iset_->end()) { // not excluded?
412 user->ReplaceInput(replacement, index);
413 induction_range_.Replace(user, instruction, replacement); // update induction
414 }
415 }
416 const HUseList<HEnvironment*>& env_uses = instruction->GetEnvUses();
417 for (auto it = env_uses.begin(), end = env_uses.end(); it != end;) {
418 HEnvironment* user = it->GetUser();
419 size_t index = it->GetIndex();
420 ++it; // increment before replacing
421 if (iset_->find(user->GetHolder()) == iset_->end()) { // not excluded?
422 user->RemoveAsUserOfInput(index);
423 user->SetRawEnvAt(index, replacement);
424 replacement->AddEnvUseAt(user, index);
425 }
426 }
427 induction_simplication_count_++;
Aart Bik807868e2016-11-03 17:51:43 -0700428 return true;
Aart Bik8c4a8542016-10-06 11:36:57 -0700429 }
Aart Bik807868e2016-11-03 17:51:43 -0700430 return false;
Aart Bik8c4a8542016-10-06 11:36:57 -0700431}
432
Aart Bik6b69e0a2017-01-11 10:20:43 -0800433void HLoopOptimization::RemoveDeadInstructions(const HInstructionList& list) {
434 for (HBackwardInstructionIterator i(list); !i.Done(); i.Advance()) {
435 HInstruction* instruction = i.Current();
436 if (instruction->IsDeadAndRemovable()) {
437 simplified_ = true;
438 instruction->GetBlock()->RemoveInstructionOrPhi(instruction);
439 }
440 }
441}
442
Aart Bik281c6812016-08-26 11:31:48 -0700443} // namespace art