Repository: Qiskit/qiskit

Issue:
Runtime Performance Regression in `SabreLayout`
### Environment

- **Qiskit Terra version**: 0.25.0
- **Python version**: 3.11
- **Operating system**: Linux


### What is happening?

When running `SabreLayout` with 0.25.0 has a large performance regression between 0.24.x and 0.25.0 that is a function of the number of gates in the circuit. The rust computation of the swap map and initial layout is as fast as before, but when we iterate over that to rewrite the `DAGCircuit` using that output from rust the runtime performance is significantly slower. For more modestly sized circuits this isn't super noticeable , but as the number of instructions in a circuit increases this overhead can be significantly higher.

### How can we reproduce the issue?

You can parse this qasm file from qasmbench: https://github.com/pnnl/QASMBench/blob/master/medium/bwt_n21/bwt_n21.qasm

```python
from qiskit import transpile, QuantumCircuit
from qiskit.providers.fake_provider import FakeCairoV2
import time
import logging
logging.basicConfig(level="INFO")

backend = FakeCairoV2()
for filename in ["bwt_n21.qasm"]:
    start = time.time()
    qc = QuantumCircuit.from_qasm_file(filename)
    parse_stop = time.time()
    print(f"parse {filename}: {parse_stop - start}")
    tqc = transpile(qc, backend, optimization_level=1)
    stop = time.time()
    print(f"{filename}: {stop - start}")
```

### What should happen?

The performance should be roughly equivalent to qiskit-terra < 0.25.0. Looking at 0.24.2 the above example `SabreLayout` took `22515.55800 ms` and 0.23.0 took `18759.72962 ms`. But I've been too impatient to wait for 0.25.0 to finish after several minutes.

### Any suggestions?

I expect the underlying issue stems from #10366 and specifically the rust<->python boundary doing unnecessary copies or conversions when we access data as we iterate over the circuit.


Discussion:
The root cause of this seems to be because of this line: https://github.com/Qiskit/qiskit-terra/blob/main/crates/accelerate/src/sabre_swap/mod.rs#L67 implementing a getter with the attribute macro like this results in `map` being cloned on each access of `SabreResult.map`. So we're basically doing a `clone()` of the full swap map for each node in the dag as we iterate over the output.

Resolving patch:
diff --git a/qiskit/transpiler/passes/routing/sabre_swap.py b/qiskit/transpiler/passes/routing/sabre_swap.py
--- a/qiskit/transpiler/passes/routing/sabre_swap.py
+++ b/qiskit/transpiler/passes/routing/sabre_swap.py
@@ -341,11 +341,14 @@ def empty_dag(node, block):
         return out
 
     def apply_inner(out_dag, current_layout, qubit_indices_inner, result, id_to_node):
-        for node_id in result.node_order:
+        node_order = result[1]
+        swap_map = result[0]
+        node_block_results = result[2]
+        for node_id in node_order:
             node = id_to_node[node_id]
             if isinstance(node.op, ControlFlowOp):
                 # Handle control flow op and continue.
-                block_results = result.node_block_results[node_id]
+                block_results = node_block_results[node_id]
                 mapped_block_dags = []
                 idle_qubits = set(out_dag.qubits)
                 for block, block_result in zip(node.op.blocks, block_results):
@@ -360,7 +363,11 @@ def apply_inner(out_dag, current_layout, qubit_indices_inner, result, id_to_node
                         mapped_block_dag,
                         mapped_block_layout,
                         block_qubit_indices,
-                        block_result.result,
+                        (
+                            block_result.result.map,
+                            block_result.result.node_order,
+                            block_result.result.node_block_results,
+                        ),
                         block_id_to_node,
                     )
 
@@ -396,9 +403,9 @@ def apply_inner(out_dag, current_layout, qubit_indices_inner, result, id_to_node
                 continue
 
             # If we get here, the node isn't a control-flow gate.
-            if node_id in result.map:
+            if node_id in swap_map:
                 process_swaps(
-                    result.map[node_id],
+                    swap_map[node_id],
                     out_dag,
                     current_layout,
                     canonical_register,


Test patch: