Skip to content
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

Micro-optimizations for Decompressor and Context #6

Merged
merged 6 commits into from
Mar 12, 2024

Conversation

maruth-stripe
Copy link
Contributor

We found current_table_size, Decompressor#read_header and Context#decode to be expensive functions while profiling code that used protocol-http2. Microbenchmarks show a ~15+% improvement in decoding performance.

Types of Changes

  • Performance improvement.

Contribution

We unrolled the loop in read_header to a case-when block. Specifically using constants for the when clauses so Ruby uses a hash lookup. We also remove some unnecessary hash lookups. We also do bookkeeping for the current table size instead of recomputing.

The changes passed all tests locally. For benchmarking we used this slightly modified version of the rfc7541 test case

require 'protocol/hpack/decompressor'
require 'yaml'


def fixtures(mode)
	Dir.glob(File.expand_path("rfc7541/*.yaml", __dir__)) do |path|
		fixture = YAML::load_file(path)
		
		if only = fixture[:only]
			next unless only == mode
		end
		
		yield fixture
	end
end

def timed(tag=nil)
	t = Time.now
	ret = yield
	d = Time.now - t
	d
end

t = 0
fixtures(:decompressor) do |example|
	example[:streams].size.times do |nth|
		t += timed do
			10_000.times do
				context = Protocol::HPACK::Context.new(huffman: example[:huffman], table_size: example[:table_size])
				buffer = String.new.b
				decompressor = Protocol::HPACK::Decompressor.new(buffer, context)
				(0...nth).each do |i|
					buffer << [example[:streams][i][:wire].gsub(/\s/, '')].pack('H*')
					decompressor.decode
				end
			end
		end
	end
end

puts t

cc @froydnj

@ioquatix ioquatix force-pushed the maruth-dont-iterate branch 2 times, most recently from 7cd6f20 to e2c1b82 Compare March 9, 2024 23:47
@ioquatix
Copy link
Member

ioquatix commented Mar 9, 2024

Wow, amazing!!

I've rebased your PR on top of a working CI, which includes external tests from protocol-http2 and async-http so we have a fair idea that this doesn't break real usage.

I'd love for you to add benchmarks to benchmarks/decompressor/... with an included readme.md which includes details of how to run the benchmark and any relevant details/analysis/results/conclusions. It's useful for future work. I'm happy for you to do this in a separate PR.

For benchmark timing, I recommend you avoid Time.now as this can be fairly imprecise, and instead consider using the Process.clock_gettime(Process::CLOCK_MONOTONIC) or similar.

NEVER_INDEXED_TYPE = {prefix: 4, pattern: 0x10}.freeze
CHANGE_TABLE_SIZE_TYPE = {prefix: 5, pattern: 0x20}.freeze
INCREMENTAL_TYPE = {prefix: 6, pattern: 0x40}.freeze
INDEXED_TYPE = {prefix: 7, pattern: 0x80}.freeze
HEADER_REPRESENTATION = {
indexed: {prefix: 7, pattern: 0x80},
Copy link
Member

Choose a reason for hiding this comment

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

Would it make sense to use the constants defined above, here?

e.g.

Suggested change
indexed: {prefix: 7, pattern: 0x80},
indexed: INDEXED_TYPE,

?

@@ -296,6 +301,11 @@ def add_to_table(command)
command.freeze

@table.unshift(command)
@cursize += entry_size(command)
Copy link
Member

@ioquatix ioquatix Mar 9, 2024

Choose a reason for hiding this comment

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

Small nit, if we are going to introduce this, can we rename it to @current_table_size? I'm okay to do this in a separate PR if you want to keep this PR hygienic.

type = nil


case (pattern & MASK_SHIFT_4)
Copy link
Member

Choose a reason for hiding this comment

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

Can you add a brief comment explaining how the lookup here works, and where the values are coming from?

header[:type] = :indexed
type = INDEXED_TYPE
else
raise CompressionError
Copy link
Member

Choose a reason for hiding this comment

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

We are missing coverage for this line - can you add a test for the failure case?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Never mind, this failure case is impossible (see comment)

@maruth-stripe
Copy link
Contributor Author

Addressed comments, I'll add a benchmarking script in a follow-up!

Copy link
Member

@ioquatix ioquatix left a comment

Choose a reason for hiding this comment

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

Great, fantastic work.

@ioquatix ioquatix merged commit f016342 into socketry:main Mar 12, 2024
20 of 22 checks passed
@ioquatix ioquatix added this to the v1.4.3 milestone Mar 12, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants