Skip to content

Use Arc instead of Box for internal expression nodes in SymbolExpr #14660

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 25, 2025

Conversation

mtreinish
Copy link
Member

Summary

This commit updates the internal construction of the SymbolExpr struct use a reference counting pointer instead of a regular pointer to a heap allocated object. The way the binary tree gets constructed internally using a normal pointer results in a lot of a copies of elements on the tree. This creates a large performance overhead for the symbol expr as we end up copying and freeing elements on the binary tree a large amount. This was the root cause of the regression reported in #14653 as the expression that gets generated by the transpiler internally ends up adding a lot of elements to global phase expression and that results in a lot of memory allocation and deallocation overhead. By using a reference counting pointer instead of creating and freeing copies of the expressions on the tree we instead only keep a single copy and just increment or decrement the reference count.

Details and comments

Fixes #14653

@mtreinish mtreinish added this to the 2.1.1 milestone Jun 23, 2025
@mtreinish mtreinish requested a review from a team as a code owner June 23, 2025 23:51
@mtreinish mtreinish added stable backport potential The bug might be minimal and/or import enough to be port to stable performance Changelog: Bugfix Include in the "Fixed" section of the changelog labels Jun 23, 2025
@qiskit-bot
Copy link
Collaborator

One or more of the following people are relevant to this code:

  • @Qiskit/terra-core

This commit updates the internal construction of the SymbolExpr struct
use a reference counting pointer instead of a regular pointer to a heap
allocated object. The way the binary tree gets constructed internally
using a normal pointer results in a lot of a copies of elements on the
tree. This creates a large performance overhead for the symbol expr as
we end up copying and freeing elements on the binary tree a large
amount. This was the root cause of the regression reported in Qiskit#14653
as the expression that gets generated by the transpiler internally ends
up adding a lot of elements to global phase expression and that results
in a lot of memory allocation and deallocation overhead. By using a
reference counting pointer instead of creating and freeing copies of
the expressions on the tree we instead only keep a single copy and
just increment or decrement the reference count.

Fixes Qiskit#14653
@coveralls
Copy link

Pull Request Test Coverage Report for Build 15837658367

Details

  • 48 of 71 (67.61%) changed or added relevant lines in 3 files are covered.
  • 28 unchanged lines in 3 files lost coverage.
  • Overall coverage decreased (-0.02%) to 88.018%

Changes Missing Coverage Covered Lines Changed/Added Lines %
crates/circuit/src/symbol_parser.rs 3 4 75.0%
crates/circuit/src/symbol_expr.rs 44 66 66.67%
Files with Coverage Reduction New Missed Lines %
crates/circuit/src/symbol_expr.rs 3 73.94%
crates/qasm2/src/lex.rs 7 91.73%
crates/qasm2/src/parse.rs 18 96.22%
Totals Coverage Status
Change from base Build 15830234190: -0.02%
Covered Lines: 84017
Relevant Lines: 95454

💛 - Coveralls

@t-imamichi
Copy link
Member

Thank you for making this fix. I tried it on my mac environment with script

qiskit_version=2.0.3
2.4967326250043698 sec
qiskit_version=2.1.0
23.56942770799651 sec
qiskit_version=2.2.0.dev0+ea61602
5.4967313330053 sec

Copy link
Contributor

@kevinhartman kevinhartman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me.

I've got one comment for a potential follow-up (hence approval without merge queue).

Value(Value),
Unary {
op: UnaryOp,
expr: Box<SymbolExpr>,
expr: Arc<SymbolExpr>,
},
Binary {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At least for Expr, I put expression variants with multiple inner Exprs within a single smart pointer, e.g. in this case:

struct Binary {
    op: BinaryOp,
    lhs: SymbolExpr,
    rhs: SymbolExpr,
}

pub enum SymbolExpr {
    Symbol(Box<String>),
    Symbol(Arc<String>),
    Value(Value),
    Unary {
        op: UnaryOp,
        expr: Arc<SymbolExpr>,
    },
    Binary(Arc<Binary>),
}

I think maybe @doichanj had this as well originally. Perhaps that could help reduce fan-out.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah lets save this for a potential follow up PR. I was thinking this should be backported for a 2.1.1 and I would like to keep the diff minimal for the backport. I think adding a struct like you're proposing and using a single pointer like you're proposing is nice change.

@mtreinish mtreinish added this pull request to the merge queue Jun 25, 2025
Merged via the queue into Qiskit:main with commit 3b26002 Jun 25, 2025
26 checks passed
@mtreinish mtreinish deleted the arc-of-the-box branch June 25, 2025 01:12
mergify bot pushed a commit that referenced this pull request Jun 25, 2025
…14660)

This commit updates the internal construction of the SymbolExpr struct
use a reference counting pointer instead of a regular pointer to a heap
allocated object. The way the binary tree gets constructed internally
using a normal pointer results in a lot of a copies of elements on the
tree. This creates a large performance overhead for the symbol expr as
we end up copying and freeing elements on the binary tree a large
amount. This was the root cause of the regression reported in #14653
as the expression that gets generated by the transpiler internally ends
up adding a lot of elements to global phase expression and that results
in a lot of memory allocation and deallocation overhead. By using a
reference counting pointer instead of creating and freeing copies of
the expressions on the tree we instead only keep a single copy and
just increment or decrement the reference count.

Fixes #14653

(cherry picked from commit 3b26002)
github-merge-queue bot pushed a commit that referenced this pull request Jun 25, 2025
…14660) (#14665)

This commit updates the internal construction of the SymbolExpr struct
use a reference counting pointer instead of a regular pointer to a heap
allocated object. The way the binary tree gets constructed internally
using a normal pointer results in a lot of a copies of elements on the
tree. This creates a large performance overhead for the symbol expr as
we end up copying and freeing elements on the binary tree a large
amount. This was the root cause of the regression reported in #14653
as the expression that gets generated by the transpiler internally ends
up adding a lot of elements to global phase expression and that results
in a lot of memory allocation and deallocation overhead. By using a
reference counting pointer instead of creating and freeing copies of
the expressions on the tree we instead only keep a single copy and
just increment or decrement the reference count.

Fixes #14653

(cherry picked from commit 3b26002)

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
@t-imamichi
Copy link
Member

@doichanj made a follow-up PR #14548

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Changelog: Bugfix Include in the "Fixed" section of the changelog performance stable backport potential The bug might be minimal and/or import enough to be port to stable
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Significant performance regression of transpilation in 2.1.0 compared to 2.0.2
5 participants