From a3819f644d366ee410a8ab0ca8632e16cbc02cb0 Mon Sep 17 00:00:00 2001 From: Guilherme Lampert Date: Tue, 20 Oct 2026 23:17:16 +1100 Subject: [PATCH] Join a partial write to the earlier value only if the lanes it keeps are read A VF write to some lanes of its variable joined the variable's earlier value whenever another lane had been written before, to keep those lanes in the same register. Lane flags accumulate along a path, so on a loop's second trip a write was joined to the last trip's value even when every lane it kept was written again before any read. Values rewritten lane by lane on each trip then stayed live for the whole program, and Tyra's stapip_cull_td_vu1, which SCE vcl compiles, ran out of VF registers for outputColor3. Work out which lanes are read after each partial write before they are written again, with a block's outputs read at its exit, and join only when one of the kept lanes is. completeLiveRanges already joins a read that needs lanes from several writes to all of them. Co-Authored-By: Claude Opus 5.5 --- src/BranchState.cpp | 12 +- src/BranchState.h | 2 + src/RegisterAllocator.cpp | 243 +++++++++++++++++------- src/RegisterAllocator.h | 9 + test/integration/test_loop_liveness.cpp | 36 ++++ 5 files changed, 237 insertions(+), 65 deletions(-) diff --git a/src/BranchState.cpp b/src/BranchState.cpp index 5bec71c..cbbca97 100644 --- a/src/BranchState.cpp +++ b/src/BranchState.cpp @@ -52,7 +52,10 @@ void BranchState::writeFloat( Token::Argument& argument ) assert( i != m_floats.end() ); } - updateDependency( argument, i->second, Alias::FLOAT, i->first, ((i->second.fields() & ~argument.fields()) != 0) ); + // a write that leaves lanes as they were keeps them in the earlier + // value's register, if one of them is read before it is written again + const unsigned int kept = i->second.fields() & ~argument.fields(); + updateDependency( argument, i->second, Alias::FLOAT, i->first, (kept & m_allocator.lanesLiveAfter( argument )) != 0 ); i->second.setFields( i->second.fields() | argument.fields() ); } @@ -429,6 +432,13 @@ void BranchState::setIntegerOutput( const std::string& name, unsigned int regNum /////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// +bool BranchState::isFloatOutput( const std::string& name ) const +{ + return m_floatOutputs.find( name ) != m_floatOutputs.end(); +} + +/////////////////////////////////////////////////////////////////////////////////////////////////////////////////////// + void BranchState::storeAddress( const Token::Argument& argument ) { assert( argument.type() == Token::Argument::INTEGER_REGISTER ); diff --git a/src/BranchState.h b/src/BranchState.h index 2ef02e9..9e8c979 100644 --- a/src/BranchState.h +++ b/src/BranchState.h @@ -161,6 +161,8 @@ class BranchState void setFloatOutput( const std::string& name, unsigned int regNumber ); void setIntegerOutput( const std::string& name, unsigned int regNumber ); + bool isFloatOutput( const std::string& name ) const; + bool applyRegisterOutputs(); const Register* inputRegister( Alias::Type type, const std::string& name ); diff --git a/src/RegisterAllocator.cpp b/src/RegisterAllocator.cpp index 4746568..7a293e6 100644 --- a/src/RegisterAllocator.cpp +++ b/src/RegisterAllocator.cpp @@ -261,6 +261,9 @@ bool RegisterAllocator::process( std::list& tokens ) blocks.push_back( std::make_pair( (*j)->current(), *j ) ); } + for( std::vector< std::pair< std::list::iterator, BranchState* > >::iterator b = blocks.begin(); b != blocks.end(); ++b ) + collectLanesLiveAfterWrites( b->first, b->second->exitPoint(), b->second ); + for( j = m_states.begin(); j != m_states.end(); j++ ) { if( !(*j) ) @@ -608,83 +611,207 @@ namespace use->depend( def ); return true; } -} -bool RegisterAllocator::completeLiveRanges( std::list::iterator begin, std::list::iterator end, BranchState* entry ) -{ - std::vector< std::list::iterator > nodes; - std::map< const Token*, unsigned int > index; - for( std::list::iterator t = begin; t != end; ++t ) + // A code block's tokens and the control flow between them: fall-through, + // branches to labels, and jr/jalr to the instruction after each bal/jalr + // that sets their register. + struct BlockFlow { - index[ &*t ] = static_cast( nodes.size() ); - nodes.push_back( t ); - } - const unsigned int count = static_cast( nodes.size() ); - if( !count ) - return true; - - // control flow: fall-through, branches to labels, and jr/jalr to the - // instruction after each bal/jalr that sets their register + std::vector< std::list::iterator > nodes; + std::vector< std::vector > successors; + std::vector< std::vector > predecessors; + bool fallsOffEnd; // the last token runs on into the block's exit + }; - std::vector< std::pair > returns; - for( unsigned int n = 0; n + 1 < count; ++n ) + void buildBlockFlow( std::list::iterator begin, std::list::iterator end, const std::map< std::string, std::list::iterator >& labels, BlockFlow& flow ) { - const Token& token = *nodes[n]; - if( !token.operand() || token.operand()->unit() != Operand::BRU ) - continue; - for( std::list::const_iterator a = token.arguments().begin(); a != token.arguments().end(); ++a ) + std::map< const Token*, unsigned int > index; + for( std::list::iterator t = begin; t != end; ++t ) { - if( (*a).flags() & Token::Argument::ADDRESS ) - returns.push_back( std::make_pair( &*a, n + 1 ) ); + index[ &*t ] = static_cast( flow.nodes.size() ); + flow.nodes.push_back( t ); } - } + const unsigned int count = static_cast( flow.nodes.size() ); + flow.successors.assign( count, std::vector() ); + flow.predecessors.assign( count, std::vector() ); + flow.fallsOffEnd = false; - std::vector< std::vector > successors( count ); - for( unsigned int n = 0; n < count; ++n ) - { - const Token& token = *nodes[n]; - bool fallsThrough = true; + std::vector< std::pair > returns; + for( unsigned int n = 0; n + 1 < count; ++n ) + { + const Token& token = *flow.nodes[n]; + if( !token.operand() || token.operand()->unit() != Operand::BRU ) + continue; + for( std::list::const_iterator a = token.arguments().begin(); a != token.arguments().end(); ++a ) + { + if( (*a).flags() & Token::Argument::ADDRESS ) + returns.push_back( std::make_pair( &*a, n + 1 ) ); + } + } - if( token.operand() && token.operand()->unit() == Operand::BRU ) + for( unsigned int n = 0; n < count; ++n ) { - fallsThrough = (token.operand()->flags() & Operand::DYNAMIC) != 0; + const Token& token = *flow.nodes[n]; + bool fallsThrough = true; - for( std::list::const_iterator a = token.arguments().begin(); a != token.arguments().end(); ++a ) + if( token.operand() && token.operand()->unit() == Operand::BRU ) { - if( !((*a).flags() & Token::Argument::BRANCH) ) - continue; + fallsThrough = (token.operand()->flags() & Operand::DYNAMIC) != 0; - if( (*a).type() == Token::Argument::IMMEDIATE ) + for( std::list::const_iterator a = token.arguments().begin(); a != token.arguments().end(); ++a ) { - std::map< std::string, std::list::iterator >::const_iterator label = m_labels.find( (*a).immediate() ); - if( label == m_labels.end() ) + if( !((*a).flags() & Token::Argument::BRANCH) ) continue; - std::map< const Token*, unsigned int >::const_iterator target = index.find( &*label->second ); - if( target != index.end() ) - successors[n].push_back( target->second ); - } - else if( (*a).type() == Token::Argument::INTEGER_REGISTER ) - { - for( unsigned int r = 0; r < returns.size(); ++r ) + + if( (*a).type() == Token::Argument::IMMEDIATE ) { - if( sameRegister( *returns[r].first, *a ) ) - successors[n].push_back( returns[r].second ); + std::map< std::string, std::list::iterator >::const_iterator label = labels.find( (*a).immediate() ); + if( label == labels.end() ) + continue; + std::map< const Token*, unsigned int >::const_iterator target = index.find( &*label->second ); + if( target != index.end() ) + flow.successors[n].push_back( target->second ); + } + else if( (*a).type() == Token::Argument::INTEGER_REGISTER ) + { + for( unsigned int r = 0; r < returns.size(); ++r ) + { + if( sameRegister( *returns[r].first, *a ) ) + flow.successors[n].push_back( returns[r].second ); + } } } } + + if( fallsThrough ) + { + if( n + 1 < count ) + flow.successors[n].push_back( n + 1 ); + else + flow.fallsOffEnd = true; + } } - if( fallsThrough && n + 1 < count ) - successors[n].push_back( n + 1 ); + for( unsigned int n = 0; n < count; ++n ) + { + for( unsigned int s = 0; s < flow.successors[n].size(); ++s ) + flow.predecessors[ flow.successors[n][s] ].push_back( n ); + } } - std::vector< std::vector > predecessors( count ); + // The lanes a named register access touches: the fields of a VF access, + // 1 for VI. A broadcast named by the opcode (maddaw, mulz) leaves its + // operand without fields; it reads the token's broadcast lane. + unsigned int accessLanes( const Token& token, const Token::Argument& argument ) + { + if( argument.type() != Token::Argument::FLOAT_REGISTER ) + return 1; + + unsigned int lanes = argument.fields(); + if( !lanes && (argument.flags() & Token::Argument::BROADCAST) ) + lanes = token.broadcast(); + return lanes ? lanes : (Token::X|Token::Y|Token::Z|Token::W); + } + + // The lanes of one VF variable each token reads and writes, and the writes + // that leave some of its lanes as they were. + struct VariableLanes + { + std::vector read; + std::vector written; + std::vector< std::pair > partialWrites; + }; +} + +void RegisterAllocator::collectLanesLiveAfterWrites( std::list::iterator begin, std::list::iterator end, BranchState* entry ) +{ + BlockFlow flow; + buildBlockFlow( begin, end, m_labels, flow ); + const unsigned int count = static_cast( flow.nodes.size() ); + const unsigned int all = Token::X|Token::Y|Token::Z|Token::W; + + std::map< std::string, VariableLanes > variables; for( unsigned int n = 0; n < count; ++n ) { - for( unsigned int s = 0; s < successors[n].size(); ++s ) - predecessors[ successors[n][s] ].push_back( n ); + const Token& token = *flow.nodes[n]; + for( std::list::const_iterator a = token.arguments().begin(); a != token.arguments().end(); ++a ) + { + if( ((*a).content() != Token::Argument::ALIAS) || ((*a).type() != Token::Argument::FLOAT_REGISTER) ) + continue; + + VariableLanes& variable = variables[ (*a).alias() ]; + if( variable.read.empty() ) + { + variable.read.assign( count, 0 ); + variable.written.assign( count, 0 ); + } + + const unsigned int lanes = accessLanes( token, *a ); + if( (*a).flags() & Token::Argument::WRITE ) + { + variable.written[n] |= lanes; + if( lanes != all ) + variable.partialWrites.push_back( std::make_pair( n, &*a ) ); + } + else + variable.read[n] |= lanes; + } } + for( std::map< std::string, VariableLanes >::const_iterator v = variables.begin(); v != variables.end(); ++v ) + { + const VariableLanes& variable = v->second; + if( variable.partialWrites.empty() ) + continue; + + // the lanes read later on some path before they are written again; the + // block's exit reads every lane of its outputs + + const unsigned int atExit = entry->isFloatOutput( v->first ) ? all : 0; + std::vector liveIn( count, 0 ); + std::vector liveOut( count, 0 ); + bool changed = true; + while( changed ) + { + changed = false; + for( unsigned int n = count; n-- > 0; ) + { + unsigned int out = (flow.fallsOffEnd && (n + 1 == count)) ? atExit : 0; + for( unsigned int s = 0; s < flow.successors[n].size(); ++s ) + out |= liveIn[ flow.successors[n][s] ]; + liveOut[n] = out; + + const unsigned int in = variable.read[n] | (out & ~variable.written[n]); + if( in != liveIn[n] ) + { + liveIn[n] = in; + changed = true; + } + } + } + + for( unsigned int w = 0; w < variable.partialWrites.size(); ++w ) + m_lanesLiveAfter[ variable.partialWrites[w].second ] = liveOut[ variable.partialWrites[w].first ]; + } +} + +unsigned int RegisterAllocator::lanesLiveAfter( const Token::Argument& write ) const +{ + std::map< const Token::Argument*, unsigned int >::const_iterator i = m_lanesLiveAfter.find( &write ); + return (i == m_lanesLiveAfter.end()) ? (Token::X|Token::Y|Token::Z|Token::W) : i->second; +} + +bool RegisterAllocator::completeLiveRanges( std::list::iterator begin, std::list::iterator end, BranchState* entry ) +{ + BlockFlow flow; + buildBlockFlow( begin, end, m_labels, flow ); + const std::vector< std::list::iterator >& nodes = flow.nodes; + const std::vector< std::vector >& successors = flow.successors; + const std::vector< std::vector >& predecessors = flow.predecessors; + const unsigned int count = static_cast( nodes.size() ); + if( !count ) + return true; + // every read and write of a named register, by variable std::map< Variable, VariableAccesses > variables; @@ -699,21 +826,9 @@ bool RegisterAllocator::completeLiveRanges( std::list::iterator begin, st const bool isFloat = ((*a).type() == Token::Argument::FLOAT_REGISTER); - // a broadcast named by the opcode (maddaw, mulz) leaves its operand - // without fields: it reads the token's broadcast lane - unsigned int lanes = 1; - if( isFloat ) - { - lanes = (*a).fields(); - if( !lanes && ((*a).flags() & Token::Argument::BROADCAST) ) - lanes = nodes[n]->broadcast(); - if( !lanes ) - lanes = Token::X|Token::Y|Token::Z|Token::W; - } - VariableAccess access; access.node = n; - access.lanes = lanes; + access.lanes = accessLanes( *nodes[n], *a ); access.dependency = (*a).dependency(); VariableAccesses& accesses = variables[ Variable( isFloat ? Alias::FLOAT : Alias::INTEGER, (*a).alias() ) ]; diff --git a/src/RegisterAllocator.h b/src/RegisterAllocator.h index b72a041..1e2242d 100644 --- a/src/RegisterAllocator.h +++ b/src/RegisterAllocator.h @@ -53,6 +53,10 @@ class RegisterAllocator const Register* floatRegister( unsigned int regNumber ) const; const Register* integerRegister( unsigned int regNumber ) const; + // the lanes of a VF write's variable that are read after the write, before + // they are written again (every lane where that is not known) + unsigned int lanesLiveAfter( const Token::Argument& write ) const; + const std::string& name(); void setDynamicThreshold( unsigned int threshold ); @@ -94,6 +98,10 @@ class RegisterAllocator // would have. bool completeLiveRanges( std::list::iterator begin, std::list::iterator end, BranchState* entry ); + // For each VF write that leaves lanes of its variable as they were, the + // lanes still read after it, for lanesLiveAfter. + void collectLanesLiveAfterWrites( std::list::iterator begin, std::list::iterator end, BranchState* entry ); + bool processCommonDirective( Token& token ); bool processAliases( Alias*& unallocated ); @@ -123,6 +131,7 @@ class RegisterAllocator std::map< std::string, std::list::iterator > m_labels; std::list< BranchState* > m_states; + std::map< const Token::Argument*, unsigned int > m_lanesLiveAfter; // void generateReadErrorReport( const Token::Argument& arg, const Token& token ); diff --git a/test/integration/test_loop_liveness.cpp b/test/integration/test_loop_liveness.cpp index 77da59e..b192621 100644 --- a/test/integration/test_loop_liveness.cpp +++ b/test/integration/test_loop_liveness.cpp @@ -6,6 +6,11 @@ // read after that branch in the next lost its register there, and a later // temporary could take it. Values loaded before a --cont loop and read // after an inner branch lost theirs the same way, which Tyra's programs do. +// +// A write to some lanes of a variable joined the variable's earlier value +// whenever another lane had been written before, even when that lane was +// written again before any read. Values rewritten lane by lane on every +// trip, like Tyra's output colours, stayed live for the whole program. #include "test_harness.h" #include "openvcl_runner.h" @@ -124,6 +129,31 @@ namespace "\t--exit\n" "\t--endexit\n"; + // Each trip writes fC's xyz and then its w before storing it, so the last + // trip's fC is dead by then, and four VF registers are enough. + const char* kPartialWrites = + "\t.init_vf vf01, vf02, vf03, vf04\n" + "\t.init_vi_all\n" + "\t.syntax new\n" + "\t.name vsmLoopLiveness\n" + "\t--enter\n" + "\t--endenter\n" + "\tiaddiu iN, vi00, 4\n" + "\tlq fA, 0(vi00)\n" + "lLoop:\n" + "\tlq fB, 1(vi00)\n" + "\tadd.xyz fC, fA, fB\n" + "\tadd.w fC, fB, fB\n" + "\tsq fC, 2(vi00)\n" + "\tlq fD, 3(vi00)\n" + "\tadd fE, fD, fA\n" + "\tmul fF, fE, fD\n" + "\tsq fF, 4(vi00)\n" + "\tiaddi iN, iN, -1\n" + "\tibne iN, vi00, lLoop\n" + "\t--exit\n" + "\t--endexit\n"; + ::test::RunResult compile(const char* source) { return ::test::run_openvcl(std::vector(), source); @@ -242,3 +272,9 @@ TEST_CASE("openvcl: a broadcast operand keeps only its lane live") ::test::RunResult result = compile(kBroadcastLane); CHECK(result.exit_code == 0); } + +TEST_CASE("openvcl: a partial write starts a new value when the lanes it keeps are dead") +{ + ::test::RunResult result = compile(kPartialWrites); + CHECK(result.exit_code == 0); +}