Repository: Qiskit/qiskit

Issue:
Performance regression in parameter binding
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues -->

### Information

- **Qiskit Terra version**:
- **Python version**:
- **Operating system**:

### What is the current behavior?

Since https://github.com/Qiskit/qiskit-terra/commit/296a745b the nightly benchmarks have flagged a regression in the benchmarks measure parameter binding. This appears quite severe as the number of parameters grow, for example:

https://qiskit.github.io/qiskit/#circuit_construction.ParameterizedCircuitBindBench.time_bind_params?machine=dedicated-benchmarking-softlayer-baremetal&os=Linux%204.15.0-46-generic&ram=16GB&p-width=20&p-gates=2048&p-number%20of%20params=2048&commits=296a745b

which is showing a 69.11x performance regression.

### Steps to reproduce the problem

Bind parameters on a circuit, the benchmark code showing the regression is here:

https://github.com/Qiskit/qiskit/blob/master/test/benchmarks/circuit_construction.py#L88-L102

### What is the expected behavior?

No performance regression

### Suggested solutions

Fix the performance regression


Discussion:

Resolving patch:
diff --git a/qiskit/circuit/library/blueprintcircuit.py b/qiskit/circuit/library/blueprintcircuit.py
--- a/qiskit/circuit/library/blueprintcircuit.py
+++ b/qiskit/circuit/library/blueprintcircuit.py
@@ -15,7 +15,7 @@
 from typing import Optional
 from abc import ABC, abstractmethod
 from qiskit.circuit import QuantumCircuit
-from qiskit.circuit.parametertable import ParameterTable
+from qiskit.circuit.parametertable import ParameterTable, ParameterView
 
 
 class BlueprintCircuit(QuantumCircuit, ABC):
@@ -93,6 +93,18 @@ def data(self):
             self._build()
         return super().data
 
+    @property
+    def num_parameters(self) -> int:
+        if self._data is None:
+            self._build()
+        return super().num_parameters
+
+    @property
+    def parameters(self) -> ParameterView:
+        if self._data is None:
+            self._build()
+        return super().parameters
+
     def qasm(self, formatted=False, filename=None):
         if self._data is None:
             self._build()
diff --git a/qiskit/circuit/library/n_local/n_local.py b/qiskit/circuit/library/n_local/n_local.py
--- a/qiskit/circuit/library/n_local/n_local.py
+++ b/qiskit/circuit/library/n_local/n_local.py
@@ -456,16 +456,6 @@ def num_parameters_settable(self) -> int:
 
         return num
 
-    @property
-    def parameters(self) -> Set[Parameter]:
-        """Get the :class:`~qiskit.circuit.Parameter` objects in the circuit.
-
-        Returns:
-            A set containing the unbound circuit parameters.
-        """
-        self._build()
-        return super().parameters
-
     @property
     def reps(self) -> int:
         """The number of times rotation and entanglement block are repeated.
diff --git a/qiskit/circuit/quantumcircuit.py b/qiskit/circuit/quantumcircuit.py
--- a/qiskit/circuit/quantumcircuit.py
+++ b/qiskit/circuit/quantumcircuit.py
@@ -193,6 +193,9 @@ def __init__(self, *regs, name=None, global_phase=0, metadata=None):
         # Parameter table tracks instructions with variable parameters.
         self._parameter_table = ParameterTable()
 
+        # Cache to avoid re-sorting parameters
+        self._parameters = None
+
         self._layout = None
         self._global_phase = 0
         self.global_phase = global_phase
@@ -492,7 +495,7 @@ def power(self, power, matrix_power=False):
             return self.repeat(power)
 
         # attempt conversion to gate
-        if len(self.parameters) > 0:
+        if self.num_parameters > 0:
             raise CircuitError('Cannot raise a parameterized circuit to a non-positive power '
                                'or matrix-power, please bind the free parameters: '
                                '{}'.format(self.parameters))
@@ -1023,6 +1026,7 @@ def _append(self, instruction, qargs, cargs):
         return instruction
 
     def _update_parameter_table(self, instruction):
+
         for param_index, param in enumerate(instruction.params):
             if isinstance(param, ParameterExpression):
                 current_parameters = self._parameter_table
@@ -1038,6 +1042,9 @@ def _update_parameter_table(self, instruction):
                                 'Name conflict on adding parameter: {}'.format(parameter.name))
                         self._parameter_table[parameter] = [(instruction, param_index)]
 
+                        # clear cache if new parameter is added
+                        self._parameters = None
+
         return instruction
 
     def _check_dup_param_spec(self, parameter_spec_list, instruction, param_index):
@@ -1907,22 +1914,25 @@ def global_phase(self, angle):
     def parameters(self):
         """Convenience function to get the parameters defined in the parameter table."""
         # parameters from gates
-        params = self._parameter_table.get_keys()
-
-        # parameters in global phase
-        if isinstance(self.global_phase, ParameterExpression):
-            params = params.union(self.global_phase.parameters)
-
-        # sort the parameters
-        sorted_by_name = sorted(list(params), key=functools.cmp_to_key(_compare_parameters))
+        if self._parameters is None:
+            unsorted = self._unsorted_parameters()
+            self._parameters = sorted(unsorted, key=functools.cmp_to_key(_compare_parameters))
 
         # return as parameter view, which implements the set and list interface
-        return ParameterView(sorted_by_name)
+        return ParameterView(self._parameters)
 
     @property
     def num_parameters(self):
         """Convenience function to get the number of parameter objects in the circuit."""
-        return len(self.parameters)
+        return len(self._unsorted_parameters())
+
+    def _unsorted_parameters(self):
+        """Efficiently get all parameters in the circuit, without any sorting overhead."""
+        parameters = set(self._parameter_table)
+        if isinstance(self.global_phase, ParameterExpression):
+            parameters.update(self.global_phase.parameters)
+
+        return parameters
 
     @deprecate_arguments({'param_dict': 'parameters'})
     def assign_parameters(self, parameters, inplace=False,
@@ -2008,7 +2018,7 @@ def assign_parameters(self, parameters, inplace=False,
 
             # check that all param_dict items are in the _parameter_table for this circuit
             params_not_in_circuit = [param_key for param_key in unrolled_param_dict
-                                     if param_key not in self.parameters]
+                                     if param_key not in self._unsorted_parameters()]
             if len(params_not_in_circuit) > 0:
                 raise CircuitError('Cannot bind parameters ({}) not present in the circuit.'.format(
                     ', '.join(map(str, params_not_in_circuit))))
@@ -2101,6 +2111,9 @@ def _assign_parameter(self, parameter, value):
         if (isinstance(self.global_phase, ParameterExpression) and
                 parameter in self.global_phase.parameters):
             self.global_phase = self.global_phase.assign(parameter, value)
+
+        # clear parameter cache
+        self._parameters = None
         self._assign_calibration_parameters(parameter, value)
 
     def _assign_calibration_parameters(self, parameter, value):


Test patch: