Skip to content

Conversation

iamcarbon
Copy link
Collaborator

@iamcarbon iamcarbon commented Sep 1, 2018

This PR retargets the library to platforms supporting System.Memory and implements an initial set of refactoring to improve performance and bring down memory usage.

This PR also:

  • Updates deps to their latest versions
  • Updates test to run on .NETSTANDARD2.1
  • Ensure that the code pages are registered on .NETSTANDARD2.0
  • Makes readers with single Extract method static (eliminating per use allocations)

@iamcarbon
Copy link
Collaborator Author

iamcarbon commented Sep 1, 2018

@drewnoakes -- This is ready for review.

I've limited my first pass utilizing the new System.Memory features to a few areas. If this gets accepted, I'll do a more through pass eliminating allocations and improving performance.

@iamcarbon
Copy link
Collaborator Author

@drewnoakes Checking-in for feedback on this.

@drewnoakes
Copy link
Owner

@iamcarbon firstly thanks for looking into this kind of optimisation. I've been watching the introduction of Span<T> and System.Memory for a while now and see the power it could bring to a library like this. Exciting times ahead.

My attempts to use spans on other projects so far have all been met with barriers when needing to call APIs that don't accept spans, so you'd end up allocating anyway and copying just to call APIs. This situation is improving, but it will require a massive increase in our minimum requirements.

The current supported TFMs net35 net46 netstandard1.3.

This PR increases them to net461 netstandard2.0.

The Java library has regularly supported versions of Java from ten years prior. Right now it supports back to Java 6 which was released in 2006. I think Java 1.5 from 2004 works too. A utility like this shouldn't exclude potential users without good cause.

So, is there adequate cause for moving to spans and dropping other frameworks?

The primary argument for making this change is improved performance and reduced allocations. Let's understand how much difference this is making through some benchmark comparisons on a suite of realistic images. I usually just use the entire sample image library, as it's large and varied enough to test systems well.

The move would have to improve performance significantly, and we'd need pretty compelling evidence of that early on in the process.

I'd also like to find some stats on usage of old frameworks.

As I mentioned to @kwhopper, I want to take a little while to re-familiarise myself with all we have right now and determine a path forward.

else if (pos != end - 1)
ret.Append("][");
}
ret.Append("]");
ret.Append(']');
Copy link
Owner

Choose a reason for hiding this comment

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

The changes of this kind could be pulled out to another PR, which I'd merge with only a quick review.

@@ -1079,7 +1079,7 @@ public string GetArtFilterEffectDescription()
for (var i = 0; i < values.Length; i++)
{
if (i == 0)
sb.Append((_filters.ContainsKey(values[i]) ? _filters[values[i]] : "[unknown]") + "; ");
sb.Append((_filters.TryGetValue(values[i], out string value) ? value : "[unknown]") + "; ");
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

No go for NET3.5.

@@ -1400,7 +1400,7 @@ private string GetFilterDescription(int tagId)
for (var i = 0; i < values.Length; i++)
{
if (i == 0)
sb.Append(_filters.ContainsKey(values[i]) ? _filters[values[i]] : "[unknown]");
sb.Append(_filters.TryGetValue(values[i], out string value) ? value : "[unknown]");
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Unfortunately, TryGetValue isn't available on .NET3.5. If we drop .NET 3.5, it's a pretty small leap to jump to .NET461.

@@ -170,7 +170,7 @@ private string GetFilterDescription(int tagId)
for (var i = 0; i < values.Length; i++)
{
if (i == 0)
sb.Append(_filters.ContainsKey(values[i]) ? _filters[values[i]] : "[unknown]");
sb.Append(_filters.TryGetValue(values[i], out string value) ? value : "[unknown]");
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

@@ -233,7 +231,7 @@ private string GetTagDataString(int tagType)
{
name = Encoding.UTF8.GetString(bytes, ofs, len);
}
res.Append(" ").Append(str).Append("(").Append(name).Append(")");
res.Append(' ').Append(str).Append('(').Append(name).Append(')');
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

@@ -77,7 +77,7 @@ public override void GetBytes(byte[] buffer, int offset, int count)
public override void Skip(long n)
{
if (n < 0)
throw new ArgumentException("n must be zero or greater.");
throw new ArgumentException("Must be zero or greater.", nameof(n));
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

[Serializable]
[TypeConverter(typeof(RationalConverter))]
#endif
public struct Rational : IConvertible, IEquatable<Rational>
public readonly struct Rational : IConvertible, IEquatable<Rational>
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto (making type readonly).

@@ -258,12 +254,10 @@ public string ToSimpleString(bool allowDecimal = true, IFormatProvider provider
/// </remarks>
/// <param name="other"></param>
/// <returns></returns>
public bool EqualsExact(Rational other) => Denominator == other.Denominator && Numerator == other.Numerator;
public bool EqualsExact(in Rational other) => Denominator == other.Denominator && Numerator == other.Numerator;
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.


public override bool Equals(object obj)
{
if (ReferenceEquals(null, obj))
return false;
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

@@ -311,7 +308,7 @@ protected string GetLensSpecificationDescription(int tagId)
if (values[0] == values[1])
sb.Append(values[0].ToSimpleString()).Append("mm");
else
sb.Append(values[0].ToSimpleString()).Append("-").Append(values[1].ToSimpleString()).Append("mm");
sb.Append(values[0].ToSimpleString()).Append('-').Append(values[1].ToSimpleString()).Append("mm");
Copy link
Owner

Choose a reason for hiding this comment

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

Ditto.

@drewnoakes drewnoakes mentioned this pull request Sep 21, 2018
@iamcarbon
Copy link
Collaborator Author

iamcarbon commented Sep 21, 2018

If we accept this on a 3.0 branch, the goal would be to improve performance by at least 2x. I don't think it's worth it otherwise.

Also curious who's left on the older frameworks and if they they're happy continuing to to use the 2.0x release.

{
private static readonly byte[] _pngSignatureBytes = { 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A };

/// <exception cref="PngProcessingException"/>
/// <exception cref="System.IO.IOException"/>
[NotNull]
public IEnumerable<PngChunk> Extract([NotNull] SequentialReader reader, [CanBeNull] ICollection<PngChunkType> desiredChunkTypes)
public static IEnumerable<PngChunk> Extract([NotNull] SequentialReader reader, [CanBeNull] ICollection<PngChunkType> desiredChunkTypes)
Copy link
Collaborator

Choose a reason for hiding this comment

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

Making all these Extract methods static means they will always be loaded at startup - even if the consumer doesn't need it. For example, if I'm not extracting on a PNG file, it will still load this code the first time the library is created.

Making it static would probably help overall efficiency if you're re-using the same library instance against many files in a loop. But I wonder if it's better to do lazy loading instead.

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

@kwhopper Are you thinking of static constructors?

Copy link
Collaborator

Choose a reason for hiding this comment

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

Not really. More of a local, initially null static variable in each MetadataReader class that is set the first time it is needed. After that, it would be reused - like a lazy loaded property.

However, that implies each of the MetadataReader classes are either all static already or only created one time from the top. That would have to be checked. Otherwise this might not be effective.

Copy link
Collaborator Author

@iamcarbon iamcarbon Sep 21, 2018

Choose a reason for hiding this comment

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

Static fields and constructors are initialized before their first use. These readers have neither.

Am I missing something? What is loaded at runtime?

@iamcarbon
Copy link
Collaborator Author

@drewnoakes -- tried cherry picking some of these improvements but .NET3.5 is holding us back. Going to abandon this for now and revisit again in the future.

Let me know if you decide to bump up the minimum targets in the future.

@iamcarbon iamcarbon closed this Sep 22, 2018
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.

3 participants