1 #include <torch/csrc/jit/passes/alias_analysis.h> 3 #include <torch/csrc/jit/script/error_report.h> 4 #include <torch/csrc/utils/memory.h> 9 bool AliasDb::shouldAnnotate(
const TypePtr& type) {
10 return type->isSubtypeOf(TensorType::get()) ||
11 type->kind() == TypeKind::ListType ||
12 type->kind() == TypeKind::TupleType ||
13 type->kind() == TypeKind::DictType || type->kind() == TypeKind::VarType ||
14 type->kind() == TypeKind::FutureType ||
15 type->kind() == TypeKind::ClassType ||
16 (type->kind() == TypeKind::OptionalType &&
17 shouldAnnotate(type->cast<OptionalType>()->getElementType()));
22 bool AliasDb::shouldAnnotate(
const Value* v) {
23 return shouldAnnotate(v->type());
26 AliasDb::~AliasDb() =
default;
28 AliasDb::AliasDb(std::shared_ptr<Graph> graph) : graph_(
std::move(graph)) {
29 memoryDAG_ = torch::make_unique<MemoryDAG>();
34 bool AliasDb::hasWildcard(
const Node* n)
const {
35 for (
const auto input : n->inputs()) {
36 if (isWildcard(input)) {
40 for (
const auto output : n->outputs()) {
41 if (isWildcard(output)) {
48 bool AliasDb::isWildcard(
const Value* v)
const {
49 return wildcards_.count(v);
52 bool AliasDb::writesTo(Node* n,
const Value* v)
const {
53 if (!shouldAnnotate(v)) {
58 return wildcardWriters_.count(n);
61 if (!elementMap_.count(v) || !writeIndex_.count(n)) {
66 if (writeIndex_.at(n).count(v)) {
71 const auto vSet = ValueSet{v};
72 return mayAlias(vSet, writeIndex_.at(n));
75 bool AliasDb::hasWriters(
const Node* n)
const {
76 for (
const auto input : n->inputs()) {
77 if (hasWriters(input)) {
81 for (
const auto output : n->outputs()) {
82 if (hasWriters(output)) {
89 bool AliasDb::hasWriters(
const Value* v)
const {
94 return numWrites_ != 0;
97 if (!elementMap_.count(v)) {
101 if (wildcardWriters_.size() > 0) {
106 if (isWriteCacheStale_) {
110 for (
const auto loc : elementMap_.at(v)->getMemoryLocations()) {
111 if (writeCache_.count(loc)) {
119 bool AliasDb::hasWrites(Node* n)
const {
120 for (
const auto input : n->inputs()) {
121 if (writesTo(n, input)) {
125 for (
const auto output : n->outputs()) {
126 if (writesTo(n, output)) {
133 bool AliasDb::writesToInputAlias(Node* n)
const {
134 std::vector<const Value*> writes;
135 for (
const auto input : n->inputs()) {
136 if (writesTo(n, input)) {
137 writes.push_back(input);
140 for (
const auto output : n->outputs()) {
141 if (writesTo(n, output)) {
142 writes.push_back(output);
147 return std::any_of(writes.cbegin(), writes.cend(), [&](
const Value* v) {
149 graph_->inputs().cbegin(),
150 graph_->inputs().cend(),
151 [&](
const Value* graphInput) {
152 return shouldAnnotate(graphInput) && mayAlias(graphInput, v);
157 void AliasDb::getWritesImpl(Block* b, ValueSet& ret,
bool recurseBlocks)
const {
158 for (
auto node : b->nodes()) {
159 getWritesImpl(node, ret, recurseBlocks);
163 void AliasDb::getWritesImpl(Node* n, ValueSet& ret,
bool recurseBlocks)
const {
164 for (
const auto input : n->inputs()) {
165 if (writesTo(n, input)) {
169 for (
const auto output : n->outputs()) {
170 if (writesTo(n, output)) {
176 for (
auto block : n->blocks()) {
177 getWritesImpl(block, ret, recurseBlocks);
183 ValueSet AliasDb::getWrites(Block* b)
const {
185 getWritesImpl(b, writes,
true);
190 bool AliasDb::writesToAlias(Node* n,
const ValueSet& vs,
bool recurseBlocks)
192 const auto writtenTo = getWrites(n, recurseBlocks);
193 return mayAlias(vs, writtenTo);
196 std::unordered_set<const Value*> AliasDb::getWrites(Node* n,
bool recurseBlocks)
199 getWritesImpl(n, writes, recurseBlocks);
203 void AliasDb::getReadsImpl(Node* n, ValueSet& ret,
bool recurseBlocks)
const {
204 for (
const auto input : n->inputs()) {
207 for (
const auto output : n->outputs()) {
212 for (
auto block : n->blocks()) {
213 for (
auto node : block->nodes()) {
214 getReadsImpl(node, ret, recurseBlocks);
220 ValueSet AliasDb::getReads(Node* n,
bool recurseBlocks)
const {
222 getReadsImpl(n, reads, recurseBlocks);
226 void AliasDb::dump()
const {
227 std::cout <<
"\n===1. GRAPH===\n";
230 std::cout <<
"\n===2. ALIAS DB===\n";
231 for (
const auto& ptrPair : elementMap_) {
232 const auto element = ptrPair.second;
233 if (element->pointsTo.size() > 0) {
234 std::cout << element->value->uniqueName() <<
" points to: ";
235 for (
const auto pointedTo : element->pointsTo) {
236 std::cout << pointedTo->value->uniqueName() <<
", ";
242 std::cout <<
"\n===3. WILDCARDS===\n";
243 for (
const auto wildcard : wildcards_) {
244 std::cout << wildcard->uniqueName() <<
", ";
248 std::cout <<
"\n===4. Writes===\n";
249 for (
const auto& pr : writeIndex_) {
250 const auto node = pr.first;
251 const auto& values = pr.second;
254 for (
const auto value : values) {
255 std::cout << value->uniqueName() <<
", ";
265 void AliasDb::makeAllAlias(
const std::vector<Value*>& values) {
266 if (values.size() > 0) {
267 giveFreshAlias(values[0]);
269 for (
const auto value : values) {
270 makePointerTo(value, values[0]);
274 void AliasDb::analyze(
const std::shared_ptr<Graph>& graph) {
279 std::map<TypeKind, std::vector<Value*>> listTypes;
280 std::unordered_map<TupleTypePtr, std::vector<Value*>> tupleTypes;
281 std::unordered_map<DictTypePtr, std::vector<Value*>> dictTypes;
282 std::unordered_map<ClassTypePtr, std::vector<Value*>> classTypes;
283 std::vector<Value*> tensors;
285 for (
auto input : graph->inputs()) {
286 auto inputType = input->type();
288 if (inputType->kind() == TypeKind::OptionalType) {
289 inputType = inputType->cast<OptionalType>()->getElementType();
292 if (inputType->isSubtypeOf(TensorType::get())) {
293 tensors.push_back(input);
294 }
else if (inputType->kind() == TypeKind::ListType) {
295 auto containedType = inputType->containedTypes().at(0);
298 if (containedType->isSubtypeOf(TensorType::get())) {
299 containedType = TensorType::get();
301 listTypes[containedType->kind()].push_back(input);
302 }
else if (inputType->kind() == TypeKind::TupleType) {
303 auto tupleType = inputType->cast<TupleType>();
304 tupleTypes[tupleType].push_back(input);
305 }
else if (inputType->kind() == TypeKind::DictType) {
306 auto dictType = inputType->cast<DictType>();
307 dictTypes[dictType].push_back(input);
308 }
else if (inputType->kind() == TypeKind::ClassType) {
309 auto classType = inputType->cast<ClassType>();
310 classTypes[classType].push_back(input);
312 AT_ASSERT(!shouldAnnotate(input));
317 for (
const auto& pr : listTypes) {
318 makeAllAlias(pr.second);
320 for (
const auto& pr : tupleTypes) {
321 makeAllAlias(pr.second);
323 for (
const auto& pr : dictTypes) {
324 makeAllAlias(pr.second);
326 for (
const auto& pr : classTypes) {
327 makeAllAlias(pr.second);
329 makeAllAlias(tensors);
331 analyze(graph->block());
334 void AliasDb::analyze(Block* block) {
335 for (
auto node : block->nodes()) {
340 void AliasDb::analyze(Node* node) {
344 if (hasWildcard(node)) {
345 wildcardNodes_.insert(node);
354 void AliasDb::analyzeImpl(Node* node) {
356 switch (node->kind()) {
358 return analyzeIf(node);
360 return analyzeLoop(node);
361 case prim::FusionGroup:
362 case prim::DifferentiableGraph:
363 return analyzeSubgraph(node);
365 return analyzeFork(node);
367 return analyzeWait(node);
369 case prim::DictConstruct:
370 case prim::ListConstruct:
371 case prim::TupleConstruct:
372 case prim::AutogradZero:
373 case prim::FusedConcat:
374 case prim::MMTreeReduce:
375 case prim::MMBatchSide:
376 case prim::BroadcastSizes:
377 case prim::ChunkSizes:
379 case prim::CreateObject:
380 return analyzeCreator(node);
381 case prim::TupleUnpack:
382 case prim::TupleIndex:
383 case prim::DictIndex:
384 case prim::TupleSlice:
385 case prim::ListUnpack:
388 return analyzeExtractor(node);
389 case prim::ConstantChunk:
390 return analyzeChunk(node);
391 case prim::BroadcastingChunk:
392 return analyzeBroadcastingChunk(node);
394 return analyzeSetAttr(node);
401 auto maybeSchema = node->maybeSchema();
403 return analyzeCreator(node);
412 AT_ASSERT(!aliasAnalysisHasSpecialCaseFor(node->kind()));
415 const auto& schema = node->schema();
416 if (schema.is_vararg() || schema.is_varret()) {
417 const auto hasMutableOutputs = std::any_of(
418 node->outputs().cbegin(),
419 node->outputs().cend(),
420 [](
const Value* output) {
return shouldAnnotate(output); });
424 if (hasMutableOutputs) {
425 throw script::ErrorReport(node->getSourceLocation())
426 <<
"Alias information not found for node. File a bug report.\n" 427 <<
"Node: " << *node <<
"\n";
432 std::unordered_map<Symbol, Value*> formalToActual;
433 for (
size_t i = 0; i < schema.arguments().size(); i++) {
434 const auto& formal = schema.arguments()[i].alias_info();
435 const auto& actualValue = node->inputs().at(i);
442 if (!shouldAnnotate(actualValue)) {
447 AT_ASSERT(formal->containedTypes().size() == 0);
450 AT_ASSERT(!formal->isWildcard())
451 const auto& formalAlias = formal->beforeSet();
454 if (formalToActual.count(formalAlias) != 0) {
459 formalToActual[formalAlias] = actualValue;
462 if (formal->isWrite()) {
463 registerWrite(actualValue, node);
468 for (
size_t i = 0; i < schema.returns().size(); i++) {
469 const auto actual = node->outputs().at(i);
470 const auto& formal = schema.returns()[i].alias_info();
473 giveFreshAlias(actual);
478 if (!shouldAnnotate(actual)) {
483 AT_ASSERT(formal->containedTypes().size() == 0);
485 if (formal->isWildcard()) {
490 for (
const auto& formalAlias : formal->beforeSets()) {
492 if (!formalToActual.count(formalAlias)) {
496 if (formal->beforeSets().size() == 1) {
497 giveFreshAlias(actual);
507 auto toAlias = formalToActual.at(formalAlias);
508 makePointerTo(actual, toAlias);
512 if (formal->isWrite()) {
513 registerWrite(actual, node);
518 void AliasDb::registerWrite(
const Value* v, Node* n) {
522 wildcardWriters_.insert(n);
526 AT_ASSERT(elementMap_.count(v));
527 writeIndex_[n].insert(v);
530 void AliasDb::analyzeIf(Node* node) {
533 const auto trueBlock = node->blocks().at(0);
534 const auto falseBlock = node->blocks().at(1);
538 for (
size_t i = 0; i < node->outputs().size(); i++) {
539 const auto nodeOutput = node->outputs()[i];
541 const auto trueOutput = trueBlock->outputs().at(i);
542 const auto falseOutput = falseBlock->outputs().at(i);
544 makePointerTo(nodeOutput, trueOutput);
545 makePointerTo(nodeOutput, falseOutput);
549 void AliasDb::analyzeLoop(Node* node) {
550 const auto bodyBlock = node->blocks().at(0);
551 const auto loopCarriedInputs = node->inputs().slice(2);
552 const auto blockInputs = bodyBlock->inputs().slice(1);
553 const auto blockOutputs = bodyBlock->outputs().slice(1);
554 AT_ASSERT(loopCarriedInputs.size() == blockInputs.size());
555 AT_ASSERT(blockOutputs.size() == node->outputs().size());
560 mapAliases(blockInputs, loopCarriedInputs);
566 mapAliases(node->outputs(), blockOutputs);
569 void AliasDb::analyzeSubgraph(Node* node) {
570 const auto subgraph = node->g(attr::Subgraph).get();
572 subgraphToOwner_.insert({subgraph, node});
574 const auto subgraphBlock = subgraph->block();
575 mapAliases(subgraphBlock->inputs(), node->inputs());
577 analyze(subgraphBlock);
582 AT_ASSERT(subgraphBlock->outputs().size() >= node->outputs().size());
583 for (
size_t i = 0; i < node->outputs().size(); i++) {
584 makePointerTo(node->outputs()[i], subgraphBlock->outputs()[i]);
589 void AliasDb::analyzeCreator(Node* node) {
590 for (Value* output : node->outputs()) {
591 giveFreshAlias(output);
597 void AliasDb::analyzeExtractor(Node* node) {
598 for (
const auto output : node->outputs()) {
599 if (shouldAnnotate(output)) {
606 void AliasDb::analyzeChunk(Node* node) {
607 for (
auto output : node->outputs()) {
608 makePointerTo(output, node->input());
615 void AliasDb::analyzeFork(Node* node) {
616 const auto subgraph = node->g(attr::Subgraph).get();
617 subgraphToOwner_.insert({subgraph, node});
619 const auto subgraphBlock = subgraph->block();
620 mapAliases(subgraphBlock->inputs(), node->inputs());
621 analyze(subgraphBlock);
624 for (
const auto output : node->outputs()) {
625 giveFreshAlias(output);
629 void AliasDb::analyzeWait(Node* node) {
630 const auto fut = node->input();
631 AT_ASSERT(fut->type()->kind() == TypeKind::FutureType);
633 if (isWildcard(fut)) {
634 for (
const auto output : node->outputs()) {
640 const auto originFuts = getMemoryLocations(fut);
641 for (
const auto originFut : originFuts) {
642 const auto subgraphNode = originFut->node();
644 const auto subgraph = subgraphNode->g(attr::Subgraph).get();
645 const auto subgraphWrites = getWrites(subgraph->block());
648 mapAliases(node->outputs(), subgraph->outputs());
672 for (
const auto write : subgraphWrites) {
673 registerWrite(write, node);
679 void AliasDb::analyzeSetAttr(Node* node) {
680 const auto self = node->inputs().at(0);
681 AT_ASSERT(self->type()->kind() == TypeKind::ClassType);
682 registerWrite(
self, node);
687 void AliasDb::analyzeBroadcastingChunk(Node* node) {
688 auto inputs = node->inputs();
689 auto outputs = node->outputs();
690 auto nchunks = node->i(attr::chunks);
691 for (
size_t index = 0; index < inputs.size(); ++index) {
694 auto output_begin = outputs.begin() + index * nchunks;
695 for (
auto it = output_begin; it != output_begin + nchunks; ++it) {
696 makePointerTo(*it, inputs.at(index));
702 void AliasDb::makePointerTo(
const Value* from,
const Value* to) {
703 if (!shouldAnnotate(from)) {
704 AT_ASSERT(!shouldAnnotate(to));
714 if (from->type()->kind() == TypeKind::OptionalType &&
715 to->type()->kind() == TypeKind::NoneType) {
720 AT_ASSERT(shouldAnnotate(from) && shouldAnnotate(to));
724 if (isWildcard(to) || isWildcard(from)) {
729 if (!isTracked(from)) {
730 giveFreshAlias(from);
732 if (!isTracked(to)) {
735 auto fromEl = elementMap_.at(from);
736 auto toEl = elementMap_.at(to);
737 memoryDAG_->makePointerTo(fromEl, toEl);
740 bool AliasDb::mayAlias(
const Value* a,
const Value* b)
const {
741 if (isWildcard(a) || isWildcard(b)) {
745 if (!elementMap_.count(a) || !elementMap_.count(b)) {
749 return memoryDAG_->mayAlias(elementMap_.at(a), elementMap_.at(b));
754 AT_ASSERT(to.
size() == from.
size());
755 for (
size_t i = 0; i < to.
size(); i++) {
756 makePointerTo(from[i], to[i]);
760 void AliasDb::giveFreshAlias(
const Value* value) {
761 if (!shouldAnnotate(value)) {
765 if (isTracked(value)) {
771 elementMap_[value] = memoryDAG_->makeFreshValue(value);
774 bool AliasDb::isTracked(
const Value* v)
const {
775 return isWildcard(v) || elementMap_.count(v);
778 bool AliasDb::moveAfterTopologicallyValid(Node* n, Node* movePoint) {
779 return tryMove(n, movePoint, MoveSide::AFTER,
false);
782 bool AliasDb::couldMoveAfterTopologically(Node* n, Node* movePoint) {
783 return tryMove(n, movePoint, MoveSide::AFTER,
true);
786 bool AliasDb::moveBeforeTopologicallyValid(Node* n, Node* movePoint) {
794 return tryMove(n, movePoint, MoveSide::BEFORE,
false);
797 bool AliasDb::couldMoveBeforeTopologically(Node* n, Node* movePoint) {
798 return tryMove(n, movePoint, MoveSide::BEFORE,
true);
811 for (
const auto user : getUsersSameBlock(n)) {
815 for (
const auto& write : aliasDb_.getWrites(n,
true)) {
816 writes_.insert(write);
818 for (
const auto& read : aliasDb_.getReads(n,
true)) {
821 if (aliasDb_.hasWildcard(n)) {
827 auto mover = nodes_.front();
828 for (
const auto user : getUsersSameBlock(mover)) {
829 const auto it = users_.find(user);
830 if (it != users_.end()) {
835 for (
const auto& write :
836 aliasDb_.getWrites(mover,
true)) {
837 const auto it = writes_.find(write);
838 if (it != writes_.end()) {
842 for (
const auto& read : aliasDb_.getReads(mover,
true)) {
843 const auto it = reads_.find(read);
844 if (it != reads_.end()) {
848 if (aliasDb_.hasWildcard(mover)) {
854 const std::list<Node*>& nodes() {
859 bool dependsOn(
Node* n)
const {
860 if (nodes_.empty()) {
864 return hasDataDependency(n) || hasMutabilityDependency(n);
868 bool hasDataDependency(
Node* n)
const {
869 if (n->isAfter(nodes_.front())) {
870 return producesFor(n);
872 return consumesFrom(n);
876 bool hasMutabilityDependency(
Node* n)
const {
879 if (numWildcards_ > 0 && aliasDb_.hasWrites(n)) {
884 if (aliasDb_.hasWildcard(n) && writes_.size() > 0) {
890 const auto nWrites = aliasDb_.getWrites(n,
true);
891 if (aliasDb_.mayAlias(nWrites, reads_)) {
896 const auto nReads = aliasDb_.getReads(n,
true);
897 if (aliasDb_.mayAlias(writes_, nReads)) {
904 bool producesFor(
Node* n)
const {
907 return users_.count(n) != 0;
911 bool consumesFrom(
Node* n)
const {
912 const auto users = getUsersSameBlock(n);
913 return std::any_of(nodes_.begin(), nodes_.end(), [&](
Node* node) {
914 return users.count(node) != 0;
921 std::unordered_set<Node*> getUsersSameBlock(
Node* n)
const {
922 std::unordered_set<Node*> users;
923 for (
const auto output : n->outputs()) {
924 for (
const auto& use : output->uses()) {
925 if (
auto sameBlock = findSameBlock(use.user, n)) {
926 users.insert(sameBlock);
940 AT_ASSERT(target->owningGraph() == n->owningGraph());
941 if (target->owningBlock() == n->owningBlock()) {
946 auto curNode = target;
947 while (curNode->owningBlock() != n->owningBlock()) {
948 curNode = curNode->owningBlock()->owningNode();
949 if (curNode ==
nullptr) {
958 std::list<Node*> nodes_;
960 std::unordered_multiset<Node*> users_;
962 std::unordered_multiset<const Value*> writes_;
963 std::unordered_multiset<const Value*> reads_;
964 size_t numWildcards_ = 0;
977 bool AliasDb::tryMove(
982 AT_ASSERT(toMove->owningBlock() == movePoint->owningBlock());
983 if (toMove == movePoint) {
992 if (toMove->isAfter(movePoint)) {
993 direction = kPrevDirection;
995 direction = kNextDirection;
998 auto curNode = toMove->next_in_graph[direction];
1000 while (curNode != movePoint) {
1001 if (workingSet.dependsOn(curNode)) {
1003 workingSet.add(curNode);
1005 curNode = curNode->next_in_graph[direction];
1025 const bool splitToMoveAndDeps =
1026 (moveSide == MoveSide::BEFORE && toMove->isBefore(movePoint)) ||
1027 (moveSide == MoveSide::AFTER && toMove->isAfter(movePoint));
1029 if (splitToMoveAndDeps) {
1031 workingSet.eraseMover();
1035 if (workingSet.dependsOn(movePoint)) {
1046 AT_ASSERT(curNode == movePoint);
1047 if (splitToMoveAndDeps) {
1049 move(toMove, movePoint, moveSide);
1052 const auto reversed =
1053 moveSide == MoveSide::BEFORE ? MoveSide::AFTER : MoveSide::BEFORE;
1054 for (
auto n : workingSet.nodes()) {
1055 move(n, curNode, reversed);
1060 for (
auto n : workingSet.nodes()) {
1061 move(n, curNode, moveSide);
1069 void AliasDb::move(
Node* toMove,
Node* movePoint, MoveSide moveSide) {
1071 case MoveSide::BEFORE:
1072 toMove->moveBefore(movePoint);
1074 case MoveSide::AFTER:
1075 toMove->moveAfter(movePoint);
1080 bool AliasDb::hasUntrackedEffects(
Node* node)
const {
1081 bool touchesWildcard =
false;
1082 if (
const auto lastWildcard = getLastWildcard()) {
1083 touchesWildcard = hasWrites(node) &&
1084 (isBeforeSameGraph(node, *lastWildcard) || node == *lastWildcard);
1087 return writesToInputAlias(node) || touchesWildcard;
1096 bool AliasDb::isBeforeSameGraph(
const Node* a,
const Node* b)
const {
1101 if (lhs->owningGraph() == rhs->owningGraph()) {
1102 return lhs->isBefore(rhs);
1104 if (!subgraphToOwner_.count(rhs->owningGraph())) {
1107 rhs = subgraphToOwner_.at(rhs->owningGraph());
1109 if (!subgraphToOwner_.count(lhs->owningGraph())) {
1112 lhs = subgraphToOwner_.at(lhs->owningGraph());
1118 auto it = std::max_element(
1119 wildcardNodes_.cbegin(),
1120 wildcardNodes_.cend(),
1121 [
this](
const Node* a,
const Node* b) {
return isBeforeSameGraph(a, b); });
1122 if (it != wildcardNodes_.end()) {
1125 return c10::nullopt;
1129 TORCH_API
bool aliasAnalysisHasSpecialCaseFor(
Symbol symbol) {
1132 const static std::unordered_set<Symbol> handled = {
1136 prim::DifferentiableGraph,
1138 prim::DictConstruct,
1139 prim::ListConstruct,
1140 prim::TupleConstruct,
1145 prim::BroadcastSizes,
1154 prim::ConstantChunk,
1155 prim::BroadcastingChunk,
1168 const static std::unordered_set<Symbol> purposefully_not_handled = {
1175 prim::AutogradAnyNonZero,
1179 return handled.count(symbol) || purposefully_not_handled.count(symbol);
1183 void AliasDb::setWildcard(
const Value* v) {
1184 if (!shouldAnnotate(v)) {
1187 wildcards_.insert(v);
1190 void AliasDb::rebuildWriteCache()
const {
1191 for (
const auto& pr : writeIndex_) {
1192 const auto& writtenValues = pr.second;
1194 for (
const auto value : writtenValues) {
1195 for (
const auto loc : elementMap_.at(value)->getMemoryLocations()) {
1196 writeCache_.insert(loc);
1200 isWriteCacheStale_ =
false;
1203 ValueSet AliasDb::getMemoryLocations(
const Value* v)
const {
1205 if (!elementMap_.count(v)) {
1209 for (
const auto el : elementMap_.at(v)->getMemoryLocations()) {
1210 ret.insert(el->value);
constexpr size_t size() const
size - Get the array size.
ArrayRef - Represent a constant reference to an array (0 or more elements consecutively in memory)...