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

Constant Encoded StringValues #64 #68

Closed
wants to merge 4 commits into from

Conversation

JunTaoLuo
Copy link

Issue #64

@dnfclas
Copy link

dnfclas commented Dec 11, 2015

Hi @JunTaoLuo, I'm your friendly neighborhood .NET Foundation Pull Request Bot (You can call me DNFBOT). Thanks for your contribution!
You've already signed the contribution license agreement. Thanks!

The agreement was validated by .NET Foundation and real humans are currently evaluating your PR.

TTYL, DNFBOT;

_bytes = null;
}

public static StringValues CreateConstant(string value, Encoding encoding)
Copy link
Author

Choose a reason for hiding this comment

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

I believe there is still some contention on the naming of this method?

Copy link
Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I don't think it should have the word "Constant" in it, as constants are a special thing already and this can't create them (despite the intent). I'm happy to just have it be Create and TryGetConstant be renamed to TryGetBytes

Choose a reason for hiding this comment

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

I think "CreateXxx" and "TryGetXxx" should have the same "Xxx" in the name. The "Xxx" doesn't have to be "Constant".

That term was chosen originally because of how it was used: internal static class Constants { public static readonly StringValues TextPlain = StringValues.CreateConstant("text/plain"); }

Actually, wasn't it originally CreateBinaryConstant? How about CreateBinary and TryGetBinary? Or like you said CreateBytes and TryGetBytes?

The problem is: we don't want people to use the Create method unless they are creating a static readonly instance used the same way you use a constant.

If you have someone call StringValues.Create(foo) per-request - as if it was just an alias for the ctor - they'll actually be increasing their allocations per-request. That's why the "Constant" hint was in there: this method is for creating constants...

Copy link
Member

Choose a reason for hiding this comment

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

If you have this CreateXxx method do you even need the associated constructor?

Copy link
Member

Choose a reason for hiding this comment

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

Create/TryGetPreEncoded?

Choose a reason for hiding this comment

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

Not really, the constructor could be internal

Copy link
Member

Choose a reason for hiding this comment

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

I don't mind CreatePreEncoded/TryGetPreEncoded

Choose a reason for hiding this comment

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

👍

@muratg
Copy link

muratg commented Dec 11, 2015

Looks good to me, apart from the CreateConstant method name contention.

cc @Tratcher

/// <param name="bytes">If successful, <paramref name="bytes"/> contains the pre-encoded bytes of the <see cref="StringValues"/>.</param>
/// <param name="encoding">If successful, <paramref name="encoding"/> contains the <see cref="Encoding"/> used to compute the pre-encoded bytes.</param>
/// <returns><c>true</c> if <see cref="StringValues"/> contains pre-encoded bytes, otherwise <c>false</c>.</returns>
public bool TryGetPreEncoded(out byte[] bytes, out Encoding encoding)

Choose a reason for hiding this comment

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

This method looks kinda gross.

Copy link
Member

Choose a reason for hiding this comment

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

Gross by design. Any recommendations for alternative designs?

Choose a reason for hiding this comment

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

Couldn't we make EncodedBytes a public property and add a remark that says its only available for pre-encoded values? Two outs feels like bad design.

Copy link
Member

Choose a reason for hiding this comment

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

It's better to keep them linked than in two separate properties. Otherwise you need to keep explaining the relationship between the two.

@Tratcher
Copy link
Member

:shipit: Do you have an initial commit for any other repo showing practical usage? Kestrel would consume it, MVC should set it.

@JunTaoLuo
Copy link
Author

Testing performance for different scenarios for StringValues implementation with class vs struct gave the following results:

Test Headers Class Implementation Struct Implementation
Known Headers Only, Using PreEncoding 863888.71 855962.316
Known Headers Only, No PreEncoding 849639.634 849207.826
Known and Unknown Headers, Using PreEncoding 721453.136 685609.094
Known and Unknown Headers, No PreEncoding 715319.652 704673.458

Each test configuration was run 5 times without error and the RPS values here are an average of those runs. Unknown Headers used are ["epyT-tnetnoC"] = "/txetnialp" and ["htgneL-tnetnoC"] = "31".

A few observations. First, although testing in the afternoon showed differences between struct vs class implementations, retesting here indicate otherwise, though class implementation seems to be slightly better in all scenarios. Also, pre-encoding headers seems to have a small benefit in most scenarios but is detrimental for unknown headers in the struct implementation.

Full results here:
stringvalues.txt

cc @halter73 @lodejard @DamianEdwards @Tratcher

@muratg
Copy link

muratg commented Dec 16, 2015

Thanks, @JunTaoLuo. These results are interesting, and slightly unexpected. Was the variance between runs small for both the class and the struct cases?

/// <param name="encoding">The <see cref="Encoding"/> to be use when computing pre-encoded bytes.</param>
/// <returns>A <see cref="StringValues"/> which contains the pre-encoded bytes.</returns>
public static StringValues CreatePreEncoded(string value, Encoding encoding)
{
Copy link
Member

Choose a reason for hiding this comment

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

We should null check the args in this scenario, we actually use them.

Copy link
Member

Choose a reason for hiding this comment

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

👍

@rynowak
Copy link
Member

rynowak commented Dec 16, 2015

It seems like the source of regression here was adding additional fields. Did we experiment with designs where we try to minimize the number of fields? For instance omitting the encoding?

@halter73
Copy link
Member

@rynowak We tried several things.

First we verified that making StringValues a class was significantly faster than the 2 field StringValues struct that existed prior to this change. We also tried out @lodejard's suggestion of making StringValues a single-field struct, but that still wasn't quite as fast (and not nearly as clean code-wise) as simply making StringValues a class.

The "Struct Implementation" results shown here are from the single-field version of the StringValues struct (which most efficient struct version according to our benchmarks). Those "Struct Implementation" benchmark results are far better than what we have in dev today. You can see the single-field struct version of StringValues here: https://gist.github.com/AspNetSmurfLab/61c40908bb316ca8f42c

Please forgive the sloppy coding in the gist, we were just trying to get benchmark results quickly.

@rynowak
Copy link
Member

rynowak commented Dec 16, 2015

@halter73 👍


public StringValues(string value)
{
_value = value;
_values = null;
_encoding = null;

Choose a reason for hiding this comment

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

You shouldn't need to initialize these anymore.

@benaadams
Copy link
Member

@halter73 I'd retest the struct vs class in RPS; it should be a lot different now with this change in aspnet/KestrelHttpServer#497

@benaadams
Copy link
Member

Could there be an in place mutator? SetPreEncoded(byte[] bytes, Encoding encoding) though would need to go back to a struct so as not to change StringValues.Empty

@benaadams
Copy link
Member

Ignore the mutator comment; it will go horribly wrong :P

@halter73
Copy link
Member

@benaadams The ClearFast change seems to make the difference between the struct/class versions of StrinvValues smaller, but the class version still performs better:

StringValues struct:

Running 15s test @ http://10.0.0.100:5001/plaintext
  32 threads and 256 connections
  Thread Stats Avg      Stdev Max   +/- Stdev
Latency     7.09ms   22.17ms 553.19ms   94.38%
    Req/Sec    30.52k     2.09k   52.48k    76.88%
  14648023 requests in 15.10s, 1.80GB read
Requests/sec: 970078.67
Transfer/sec:    122.12MB

Running 15s test @ http://10.0.0.100:5001/plaintext
  32 threads and 256 connections
  Thread Stats Avg      Stdev Max   +/- Stdev
Latency     9.91ms   30.88ms 400.11ms   92.26%
    Req/Sec    30.80k     2.13k   42.56k    68.38%
  14793210 requests in 15.10s, 1.82GB read
Requests/sec: 979714.96
Transfer/sec:    123.33MB

Running 15s test @ http://10.0.0.100:5001/plaintext
  32 threads and 256 connections
  Thread Stats Avg      Stdev Max   +/- Stdev
Latency    10.63ms   33.91ms 381.54ms   93.81%
    Req/Sec    30.84k     2.43k   55.73k    77.87%
  14787069 requests in 15.10s, 1.82GB read
Requests/sec: 979280.70
Transfer/sec:    123.28MB

StringValues class:

Running 15s test @ http://10.0.0.100:5001/plaintext
  32 threads and 256 connections
  Thread Stats Avg      Stdev Max   +/- Stdev
Latency     5.30ms   16.14ms 296.50ms   94.95%
    Req/Sec    32.74k     2.18k   59.47k    83.09%
  15704197 requests in 15.10s, 1.93GB read
Requests/sec: 1040031.02
Transfer/sec:    130.92MB

Running 15s test @ http://10.0.0.100:5001/plaintext
  32 threads and 256 connections
  Thread Stats Avg      Stdev Max   +/- Stdev
Latency     5.81ms   21.21ms 388.86ms   95.70%
    Req/Sec    32.71k     2.29k   66.62k    83.89%
  15684105 requests in 15.10s, 1.93GB read
Requests/sec: 1038689.40
Transfer/sec:    130.76MB

Running 15s test @ http://10.0.0.100:5001/plaintext
  32 threads and 256 connections
  Thread Stats Avg      Stdev Max   +/- Stdev
Latency     6.10ms   26.08ms 515.19ms   93.51%
    Req/Sec    32.82k     2.28k   60.88k    83.33%
  15738015 requests in 15.10s, 1.93GB read
Requests/sec: 1042350.33
Transfer/sec:    131.22MB

@benaadams
Copy link
Member

@halter73 can't argue with that.

However, if going class, then mutators would be nice; though Empty would have to change to

public static StringValues Empty => new StringValues(EmptyArray);

Which night have a perf impact; and null StringValues (rather than Empty) would have a problem :-/
Unless getters (like headers) also did a private set to Empty when null; however something might that may already be needed as null will now propagate round the system.

e.g.

StringValue.IsNullOrEmpty(null) before would return true, now it will throw.

What happens with context.Response.Headers[unknownHeaderName] that now returns null so all the StringValues instance methods will throw as they are called on a null reference.

The implicit conversions need to change to accept null values e.g.

public static implicit operator string (StringValues values)
{
    return values.GetStringValue();
}
public static implicit operator string[] (StringValues value)
{
    return value.GetArrayValue();
}

needs to change to

public static implicit operator string (StringValues values)
{
    if (values == null) return null;
    return values.GetStringValue();
}
public static implicit operator string[] (StringValues value)
{
    if (value == null) return EmptyArray;
    return value.GetArrayValue();
}

etc...

@benaadams
Copy link
Member

@halter73 is also an issue with the current struct's enumerator #74 which is likely to make a difference.

Both with StringValues being readonly and it passed to ctor by value (if its getting bigger)

@benaadams
Copy link
Member

I'd like to suggest a hold on this for now; there's a faster CopyFromAscii in Kestrel aspnet/KestrelHttpServer#527 and an even faster one to come when only pooled blocks are used aspnet/KestrelHttpServer#525 as one of the fixed can be dropped and the blocks can have raw pointers stored; but also Array.Copy and Buffer.BlockCopy in the framework are slow for byte arrays < 512 bytes; so this may end up being slower than the CopyFromAscii changes (though there are some workarounds for it)

@JunTaoLuo JunTaoLuo force-pushed the johluo/constant-stringvalues branch from 9020e65 to 4671bc8 Compare January 21, 2016 20:03
@JunTaoLuo
Copy link
Author

After the many kestrel optimizations, testing the performance of this optimization showed that it makes little difference. I saw ~1% improvement in the plaintext benchmark and after I added 16 more headers the performance improvement was bigger but less stable, hovering between 4-7% (ee aspnet/KestrelHttpServer#584 for more details). Note that the headers added were for illustration only and in real scenarios, it does not make sense to pre-encode all of them. In the end, we decided that this optimization is no longer relevant and will not be made.

@JunTaoLuo JunTaoLuo closed this Jan 21, 2016
@JunTaoLuo JunTaoLuo deleted the johluo/constant-stringvalues branch January 22, 2016 19:35
natemcmaster pushed a commit that referenced this pull request Nov 6, 2018
using statement duplicated
natemcmaster pushed a commit that referenced this pull request Nov 16, 2018
natemcmaster pushed a commit that referenced this pull request Nov 16, 2018
Fix #68 - use trace for header logging
@halter73 halter73 mentioned this pull request Apr 10, 2019
@ghost ghost locked as resolved and limited conversation to collaborators May 30, 2023
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

10 participants