Skip to content
This repository has been archived by the owner on Jun 22, 2023. It is now read-only.

Use Lazy<T> for GeometryServiceProvider.Instance. #40

Closed
wants to merge 1 commit into from

Conversation

airbreather
Copy link
Member

On platforms that support it, anyway.

I got uncomfortable with how much discussion there was about performance, compared to how few measurements were being thrown around. So I threw all the implementations we've been discussing and ran a benchmark. Here were the results:

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.16299.371 (1709/FallCreatorsUpdate/Redstone3)
Intel Core i7-6850K CPU 3.60GHz (Skylake), 1 CPU, 12 logical and 6 physical cores
Frequency=3515626 Hz, Resolution=284.4444 ns, Timer=TSC
  [Host]     : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0
  Job-YJMWXB : .NET Framework 4.7.1 (CLR 4.0.30319.42000), 64bit RyuJIT-v4.7.2633.0

Server=True  
Method Implementation Preinit Mean Error StdDev
RunBench NoChange False 251.13 ms 0.6212 ms 0.5507 ms
RunBench NoChange True 269.15 ms 1.7961 ms 1.6801 ms
RunBench PR39 False 71.01 ms 1.3958 ms 2.2140 ms
RunBench PR39 True 68.93 ms 1.3769 ms 2.2234 ms
RunBench LazyT False 72.46 ms 1.3433 ms 2.4223 ms
RunBench LazyT True 68.50 ms 1.3520 ms 3.1065 ms
RunBench LazyInitializer False 83.68 ms 1.6537 ms 4.4707 ms
RunBench LazyInitializer True 73.54 ms 1.4641 ms 2.4861 ms

The specific implementations:

  • NoChange is... well, no change.
  • PR39 is from Improved performance of the Instance property #39
  • LazyT is this PR.
  • LazyInitializer: LazyInitializer.EnsureInitialized(ref _instance, ref _initialized, ref _lockObject2, ReflectInstance);, where _lockObject2 is a non-volatile non-readonly object field pre-initialized to new object().

Appendix A has the exact details about how I ran this, but the short version is that the timings are from running 1024 concurrent threads, each of which repeats 1024 times: "fetch GeometryServiceProvider.Instance, throw if it was null, then yield".

This was designed to show the absolute worst case of contention that I can think of. Both the absolute and the relative numbers may be interesting for discussion.

Finally, note that net20, net35-client, and net35-cf would sit on the NoChange lines, other targets would sit on the LazyT lines, and the .NET Standard ones would be on the LazyT line with "Preinit = true".

Appendix A: Benchmark program

DummyGeometryServices is a dummy implementation of IGeometryServices in the benchmark EXE.

    public class Program
    {
        private readonly Thread[] threads = new Thread[1024];

        public enum Impl
        {
            NoChange,
            PR39,
            LazyT,
            LazyInitializer,
        }

        [Params(Impl.NoChange, Impl.PR39, Impl.LazyT, Impl.LazyInitializer)]
        public Impl Implementation { get; set; }

        [Params(true, false)]
        public bool Preinit { get; set; }

        [Benchmark]
        public void RunBench()
        {
            if (this.Preinit)
            {
                GeometryServiceProvider.PreInitBench(new DummyGeometryServices());
            }

            Func<IGeometryServices> getInstance = null;
            switch (this.Implementation)
            {
                case Impl.NoChange:
                    getInstance = () => GeometryServiceProvider.InstanceLock;
                    break;

                case Impl.PR39:
                    getInstance = () => GeometryServiceProvider.InstancePR39;
                    break;

                case Impl.LazyT:
                    getInstance = () => GeometryServiceProvider.InstanceLazyT;
                    break;

                case Impl.LazyInitializer:
                    getInstance = () => GeometryServiceProvider.InstanceLazyInitializer;
                    break;
            }

            for (int i = 0; i < this.threads.Length; i++)
            {
                this.threads[i] = new Thread(RunThread) { IsBackground = true };
            }

            foreach (Thread t in this.threads)
            {
                t.Start(getInstance);
            }

            foreach (Thread t in this.threads)
            {
                t.Join();
            }

            GeometryServiceProvider.ResetBench();
        }

        private static void Main(string[] args) => BenchmarkRunner.Run<Program>(ManualConfig.Create(DefaultConfig.Instance).With(Job.Default.WithGcServer(true)));

        private static void RunThread(object obj)
        {
            Func<IGeometryServices> callback = (Func<IGeometryServices>)obj;
            for (int i = 0; i < 1024; i++)
            {
                if (callback() == null)
                {
                    throw new InvalidOperationException();
                }

                Thread.Yield();
            }
        }
    }

On platforms that support it, anyway.
private static IGeometryServices _instance;
#if HAS_SYSTEM_LAZY_T
private static readonly Lazy<IGeometryServices> ReflectedInstance = new Lazy<IGeometryServices>(ReflectInstance);
#else
private static readonly object LockObject = new object();
Copy link
Contributor

Choose a reason for hiding this comment

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

A Lazy<T> already has a lock inside it when it's initialized using that constructor.

Copy link
Member Author

@airbreather airbreather Apr 14, 2018

Choose a reason for hiding this comment

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

With this change, LockObject is only used on runtimes TargetFrameworks that don't have access to Lazy<T> (net20, net35-client, net35-cf).

Copy link
Contributor

Choose a reason for hiding this comment

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

Yeah, you're right. You use lock around ReflectInstance and not in it.

@airbreather
Copy link
Member Author

@FObermaier please take a look, this meets the goal you'd mentioned here:

If there are other reasons for the lock that I'm not aware of, I'd hesitate to introduce a restriction on the frameworks we can use, so it'd be either Lazy<T> or the code as is.

Specifically net20, net35-client, and net35-cf would all wind up using the code as-is, but everybody else can skip the lock in the common case where it's been initialized already with just a few lines of change (admittedly, I cheated a little and snuck the HAS_SYSTEM_LAZY_T part into d5cdbf2).

@FObermaier
Copy link
Member

@YohDeadfall, @roji, do you have any objections using this PR to address your problem?

@YohDeadfall
Copy link
Contributor

The test isn't representative because:

  1. there are plenty of unrelated actions which execution time also counts;
  2. Improved performance of the Instance property #39 has other optimizations which makes it a bit faster.

@airbreather
Copy link
Member Author

airbreather commented Apr 17, 2018

there are plenty of unrelated actions which execution time also counts;

The actions taken in the benchmark that I'd run are intended to create a best-case situation to showcase the claimed benefits that removing lock from the fast-path would have in a contention-heavy scenario.

Would you like a complete, buildable, runnable version of the sample that you could experiment with on your own to test a hypothesis that you have about something in here tainting the benchmark? If so, I'd be happy to oblige: GeoAPI.zip. Note that you have to build the GeoAPI project directly before IntelliSense will work in GeoAPI.Bench.

#39 has other optimizations which makes it a bit faster.

Other than the couple of additional commits that were added after I'd gathered the above numbers, I made every effort for PR39 to faithfully represent that branch (going as far as to copy-paste your entire version of InitializeInstance). See the above zip file.

If you'd like, I can re-run it with those two additional commits added (or you can do so yourself). Otherwise, the above measurements suggest that any effect from the "other optimizations" contributes a benefit that's smaller than the measured standard deviation of these samples; at best, if the other changes do have an impact, I have no evidence that their combined impact is stronger than 2 milliseconds per application instance.

@YohDeadfall
Copy link
Contributor

I mostly have made benchmarks, but due to temporary unavailability of NuGet I can't restore packages and finish them. When it's done, I push them into a new branch in my form and post a link to it.

@YohDeadfall
Copy link
Contributor

YohDeadfall commented Apr 19, 2018

If you want to run the benchmark on multiple cores, use xunit.perfromance with which you'll be able to construct threads, set affinity and start them before measurement. Each thread should wait for something (volatile bool for example) until you start measurement in which you should signal to the threads and wait. But there is no reason to write such a benchmark, since we know a looser :)

As promised I have made a benchmark to test two scenarios:

  1. when the Instance property is auto initialized;
  2. when the Instance property is set by an user.
public class AutoInitialized
{
    [Benchmark]
    public IGeometryServices Locks()
    {
        UseLocks.Reset();
        return UseLocks.Instance;
    }

    [Benchmark]
    public IGeometryServices PoolRequest39()
    {
        UsePoolRequest39.Reset();
        return UsePoolRequest39.Instance;
    }

    [Benchmark]
    public IGeometryServices Lazy()
    {
        UseLazy.Reset();
        return UseLazy.Instance;
    }

    [Benchmark]
    public IGeometryServices LazyInitializer()
    {
        UseLazyInitializer.Reset();
        return UseLazyInitializer.Instance;
    }
}

public class UserInitialized
{
    private static readonly DummyGeometryServices s_dummy = new DummyGeometryServices();

    [GlobalSetup]
    public void Setup()
    {
        UseLocks.Instance = s_dummy;
        UsePoolRequest39.Instance = s_dummy;
        UseLazy.Instance = s_dummy;
        UseLazyInitializer.Instance = s_dummy;
    }

    [Benchmark]
    public IGeometryServices Locks()
        => UseLocks.Instance;

    [Benchmark]
    public IGeometryServices PoolRequest39()
        => UsePoolRequest39.Instance;

    [Benchmark]
    public IGeometryServices Lazy()
        => UseLazy.Instance;

    [Benchmark]
    public IGeometryServices LazyInitializer()
        => UseLazyInitializer.Instance;
}

public static class UseLocks
{
    private static volatile IGeometryServices s_instance;
    private static readonly object s_lock = new object();

    public static IGeometryServices Instance
    {
        get
        {
            lock (s_lock)
            {
                return s_instance ?? (s_instance = ReflectInstance());
            }
        }
        set
        {
            lock (s_lock)
            {
                s_instance = value;
            }
        }
    }

    public static void Reset()
        => s_instance = null;

    private static IGeometryServices ReflectInstance()
}

// etc.

The full code can be found in 921a147.

Environment

BenchmarkDotNet=v0.10.14, OS=Windows 10.0.17112
Intel Xeon CPU E5-2687W 0 3.10GHz, 2 CPU, 32 logical and 16 physical cores
Frequency=3038570 Hz, Resolution=329.1022 ns, Timer=TSC
.NET Core SDK=2.0.3
  [Host]     : .NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT
  DefaultJob : .NET Core 2.0.3 (CoreCLR 4.6.25815.02, CoreFX 4.6.25814.01), 64bit RyuJIT

AutoInitialized

Method Mean Error StdDev
Locks 331.3 us 2.474 us 2.193 us
PoolRequest39 313.8 us 1.482 us 1.387 us
Lazy 315.1 us 2.468 us 2.188 us
LazyInitializer 323.2 us 6.265 us 6.153 us

UserInitialized

Method Mean Error StdDev
Locks 17.964 ns 0.0109 ns 0.0096 ns
PoolRequest39 1.596 ns 0.0032 ns 0.0028 ns
Lazy 1.612 ns 0.0899 ns 0.0841 ns
LazyInitializer 1.631 ns 0.1166 ns 0.1432 ns

Conslusion

As you can see there is no big difference between pool request #39, Lazy<T> and LazyInitializer approaches because they all use volatile read. To implement a bootsatrapper you need a lock, so only two options are left: #39 and LazyInitializer. The first one requires a bit more code and a bit faster. The second one doesn't allow you to make a readonly field and requires an additional bool field. So it's more about personal preferences I think.

@airbreather
Copy link
Member Author

airbreather commented Apr 19, 2018

I guess I'm missing the point of this benchmark that you've run. Unless I'm misunderstanding it, it seems to suggest that implementing either #39 or #40, compared to doing nothing, can save us about 16 nanoseconds per access of GeometryServiceProvider.Instance access after it's been initialized. In the broader context of GeoAPI / NetTopologySuite, where I've seen a single Union operation take several minutes (and where my gut tells me that there's pretty much nothing useful that we do that takes less than a microsecond), 16 nanoseconds is not enough to raise an eyebrow, especially since a performance-conscious application can implement its own cache that doesn't even need volatile.

If you want to run the benchmark on multiple cores, use xunit.perfromance with which you'll be able to construct threads, set affinity and start them before measurement. Each thread should wait for something (volatile bool for example) until you start measurement in which you should signal to the threads and wait. But there is no reason to write such a benchmark, since we know a looser :)

The point of my initial benchmark was to prove (to myself, at least) that there may be a problem to solve. Personally, I find it very difficult to propose a solution to a problem that hasn't been proven to exist. This is especially the case when it comes to performance. Especially when there are alternative solutions being proposed. So before I could allow myself to submit a pull request, I needed to submit something that proves the existence of a measurable problem, and that this is a solution to that problem.

I recognize that the approach I took to simulate contention for the lock is not as robust and precise as it would need to be for a different situation. However, I do not see any reason to believe that the results it generated are invalid, nor do I see any reason to continue throwing benchmark results around: @FObermaier specifically asked if you had any objections to using this PR to address your problem, so if you don't have objections on the grounds of speed, then it seems to me that there's no reason to bring up any benchmarks.

However, this part of your response does seem to stand out as an objection on another ground, and I wish you'd spent more time elaborating on it:

To implement a bootsatrapper you need a lock, so only two options are left: 39 and LazyInitializer.

It sounds like you're claiming that this one is incorrect in some way; can you please help me understand what your objection is? If possible, please try to meet the following criteria, to try to avoid holding up a solution for too much longer:

  1. Include a step-by-step sequence of actions that could cause an undesired outcome on computers that exist today in the real world (feel free to pick something other than x86 / x64, since I've only seriously evaluated this with the x86 / x64 memory model in mind), and
  2. Explain why you think that that outcome is undesired.

@YohDeadfall
Copy link
Contributor

Looks like we didn't understand each other correctly. Okay, I'll try to explain myself better :)

I guess I'm missing the point of this benchmark that you've run.

The idea of my benchmark to know execution time of each scenario. With the numbers we have now it's possbile to know how long it would take when multiple threads are running. For example, for the Locks case when Instance is set the execution time is 17.964 ns, and we would like to know how long it would take when 4 threads try to access the property in parallel. Since lock allows only one thread at the same time to step into its body, the answer can be calculated as 17.964 multiplied by 4 which is 71.856 ns. Simple math, but it allows to get results which you tried to measure.

The point of my initial benchmark was to prove (to myself, at least) that there may be a problem to solve.

I'm pretty sure that you know that lock manages threads to access a critical section sequentially. Therefore, I had no thoughts that you would like to prove it :)

It sounds like you're claiming that this one is incorrect in some way; can you please help me understand what your objection is?

Okay, you want to have a bootstrapper (see #41). It should work like the current initialization algorithm, but with an user provided factory. How can you do that using Lazy<T>? That's a problem. You need to have a lock, a global lock to prevent parallel initialization by the bootstrapper and the reflection based initializer.

private static volatile IGeometryServices s_instance;
private static readonly object s_lock = new pbject();

public static bool Initialize(Func<IGeometryServices> factory)
{
    if (s_instance != null)
        return false;

    lock (s_lock)
        return Interlocked.CompareExchange(ref s_instance, factory(); null) == null;
}

private static IGeometryServices InitializeUsingReflection()
{
    lock (s_lock)
    {
        var instance = s_instance;
        if (instance != null) return instance;
        // other stuff
    }
}

In the code above only one factory can be executed (other stuff or factory). The same could be achieved using LazyInitializer, but the code would be more complicated.

Have I answered all your questions?

/cc @roji

@roji
Copy link

roji commented Apr 22, 2018

I haven't had time to follow this discussion in detail...

To be honest, this seems like a huge over-complicated conversation about something that seems quite simple to me... I'll try to go back to the basics.

  1. You have a frequently-accessed global singleton (GeometryServiceProvider.Instance), and the current implementation locks all reads to it. This seems like an obvious potential contention problem; as removing the contention is so easy, I wouldn't even go to the trouble of producing benchmarks. There's no real reason not to just fix it.
  2. I see three small issues with this PR (Use Lazy<T> for GeometryServiceProvider.Instance. #40):
    • It creates different code paths (and so different behavior) depending on the target framework (i.e. whether Lazy is available or not).
    • It doesn't solve the contention problem for the older frameworks
    • It uses two state variables (ReflectedInstance and _instance) where only one is actually needed.
  3. The above disadvantages aren't huge - but then I can't really see the what Lazy brings to the table - why not just do volatile here?

I think everyone agrees that locking on every read is not a good situation to be in (I may be wrong). If so, then are we just discussing Lazy vs. simple volatile?

@airbreather
Copy link
Member Author

airbreather commented Apr 23, 2018

@YohDeadfall:

Okay, you want to have a bootstrapper (see #41). It should work like the current initialization algorithm, but with an user provided factory. How can you do that using Lazy<T>?

I've been figuring that the combination of #40 and #41 can be done like so:

private static readonly Lazy<IGeometryServices> ReflectedInstance = new Lazy<IGeometryService>(ReflectInstance);

private static volatile IGeometryServices _instance;

public static IGeometryServices Instance
{
    get => _instance ?? ReflectedInstance.Value;
    set => _instance = value ?? throw new ArgumentNullException(nameof(value));
}

public static bool SetExplicitValueIfNotYetSetExplicitly(IGeometryServices newValue) =>
    Interlocked.CompareExchange(ref _instance,
                                newValue ?? throw new ArgumentNullException(nameof(newValue)),
                                null) == null;

When the instance value is null, then nobody's set an explicit value yet, so the property getter should return the Lazy<T> value and the downstream library's (NetTopologySuite's) automatic initialization should kick in to initialize it for everybody.

When the instance value is non-null, then somebody's set an explicit value, so the property getter should use that, and the downstream library's (NetTopologySuite's) automatic initialization should not overwrite what's already been explicitly initialized.

That's a problem. You need to have a lock, a global lock to prevent parallel initialization by the bootstrapper and the reflection based initializer.

Lazy<T> (and, for that matter, the code as-is) makes sure that we only run the reflection-based initializer code when the getter is accessed for the first time before any explicit initialization. It uses its own global lock to do so.

Reflection-based initializer should only run if someone accesses the getter before the value has been set explicitly.

@roji:

To be honest, this seems like a huge over-complicated conversation about something that seems quite simple to me.

I agree. I'm surprised it's gotten as deep as it has.

You have a frequently-accessed global singleton (GeometryServiceProvider.Instance), and the current implementation locks all reads to it.

It's not a "singleton" in the traditional sense, because there can be any arbitrary number of instances at once.

I also question the "frequently-accessed" part of this... I don't expect consumers who are concerned about either correctness or performance to access a global mutable variable in any sort of loop where they expect it to remain the same. Edit: ehh... that's a weak argument. What I should have said is an echo of something I've said a few times before: it seems like, in any code path that accesses this global variable for any real purpose, the cost of a lock is very unlikely to be relevant, given how little work the CPU needs to do within the critical section (it's a single field access).

This seems like an obvious potential contention problem

lock, around what's practically always going to be a single field access, used for the kinds of operations that this is used for, does not jump out as "obvious" to me. Even if it's accessed in a loop, what in the world is the consumer doing with the instance where a lock around a field access is slowing things down?

removing the contention is so easy [...]. There's no real reason not to just fix it.

lock is the C# way to drastically cut down on how much developers have to think about thread safety in the face of hardware and software environments that do everything under the sun to minimize the number of times that the execution needs to stall waiting for memory access. Removing an existing lock around a shared publicly mutable variable and replacing it with something less aggressive demands (to me, anyway) careful consideration.

Doing so here, when I had to really reach to prove that any kind of problem might exist (Thread.Yield() is extremely cheap compared to even the simplest thing that someone might do with the result, and ), is frustrating to me personally. I'm trying to move past my frustrations, though.

I see three small issues with this PR (#40):

Well the first and second are the same, but yes, .NET Framework 2.0, 3.5 Client, and 3.5 Compact Framework cannot benefit from this change.

And yeah there are two state variables, but I don't think we can get away with fewer variables in our own code... we trade away an object used for our own locking, in exchange for a Lazy<T>.

The above disadvantages aren't huge

I agree.

I can't really see the what Lazy brings to the table - why not just do volatile here?

re: "just volatile", it's not quite enough: without any locking, multiple threads entering the getter in the "we need to initialize it" path at the same time will race to initialize the result, and different threads might wind up overwriting the instance with a different result; different calls may therefore different instances without something done explicitly to stop that.

There are ways to address this problem. Lazy<T> is one of them. Reimplementing parts of it is another. Allowing pre-initialization getter accesses to all enter the reflection-based initialization to get an instance and using Interlocked.CompareExchange to make sure that everybody receives the same instance can be yet another. Ignoring it and saying "meh, it won't happen" is another. Lazy<T> is the one that I chose here.

I think everyone agrees that locking on every read is not a good situation to be in (I may be wrong).

Close enough.

If so, then are we just discussing Lazy vs. simple volatile?

I hope not. I hope that what we're doing is discussing this PR as opposed to doing nothing, since the timeline on this one has been, as I see it:

  1. @YohDeadfall submitted Improved performance of the Instance property #39 with a proposed improvement to GeometryServiceProvider that touched several lines in the file.
  2. I reviewed it and signaled that I was OK with merging it, despite concerns I'd voiced that it seemed like we were fixing a non-problem, and asked if @FObermaier had any concerns of his own.
  3. After some more comments, @FObermaier said a comment that included the line "it'd be either Lazy<T> or the code as is.".
  4. I figured that, rather than argue that point, it would be quicker to just write up and submit this small simple fix which also solves the same problem on all the frameworks that npgsql targets.
  5. @FObermaier asked if there were any objections to using this PR to solve the problem.
  6. @YohDeadfall responded with what I've been interpreting as objections, and I've been attempting to address the objections as they come in.
  7. @roji entered the discussion, presumably confused about how we got to this point from that comment.

One thing that I've been trying not to discuss is the best way that GeometryServiceProvider could be implemented, because I think that the best implementation possible would be for it not to exist: it's only used in NetTopologySuite (not GeoAPI, not ProjNet4GeoAPI), and so it's hard to imagine anybody in a situation where they can't just set the one in NetTopologySuite, in which case it would be better for that library to initialize its own instance inline, since it can do that with a constructor call instead of scanning every type in the app domain.

@roji
Copy link

roji commented Apr 23, 2018

I'm sorry I don't have time for a more complete response but here's a quick one on 2 points... Will be more available later to continue the conversation.

I can't really see the what Lazy brings to the table - why not just do volatile here?

re: "just volatile", it's not quite enough: without any locking, multiple threads entering the getter in the "we need to initialize it" path at the same time will race to initialize the result, and different threads might wind up overwriting the instance with a different result; different calls may therefore different instances without something done explicitly to stop that.

I may be wrong, but the code in #39 first looks at s_instance (the volatile variable), and if it's non-null (i.e. already initialized either by the user or by a previous reflection-based initialization), returns it. If the field is already null, it calls into InitializeInstance() which does lock. Note also that InitializeInstance() checks again, inside the lock, whether s_instance has been set in the meantime, so it will never go into reflection initialization unless s_instance has been verified to be null, within the lock.

To me it seems like a simple implementation of double-checked locking, which is fairly standard for singletons (or singleton-like situations like this).

As I wrote above, I don't really have that much against using Lazy here, but as long as we're discussing synchronization intricacies it may good to clearly state the scenario which you think will fail if the code in #39 is used.

I also question the "frequently-accessed" part of this...

That's fair enough. I have little (or more precisely no) knowledge of NTS and how frequently this field actually gets read - I was under the impression that it isn't trivial. And any sort of a global singleton-like value automatically feels like it could become a bottleneck in some user scenarios. Once again, I think the techniques for avoiding contention are well-known and tested, but if you feel this field really doesn't get accessed enough to care, then there's no reason to look at Lazy<T> either, in my opinion.

@airbreather
Copy link
Member Author

airbreather commented Apr 23, 2018

I can't really see the what Lazy brings to the table - why not just do volatile here?

re: "just volatile", it's not quite enough: without any locking, multiple threads entering the getter in the "we need to initialize it" path at the same time will race to initialize the result, and different threads might wind up overwriting the instance with a different result; different calls may therefore different instances without something done explicitly to stop that.

I may be wrong, but the code in #39 [...] does lock.

Sorry, I may have misinterpreted your comment. When you said "just volatile", I thought you may have been referring to something other than #39.

as long as we're discussing synchronization intricacies it may good to clearly state the scenario which you think will fail if the code in #39 is used.

I don't think such a scenario exists. I believe that #39 is an acceptable solution (or, well, it was when I had reviewed it previously; since then, more lines were changed), and my previous comment said as much. OK, if I'm being completely honest, I was a little annoyed at the changes that weren't related to making the getter avoid a lock in the fast-path, like renames and inlining a method, but I got over that.

See the timeline in my previous comment for why I opened #40 despite that.

if you feel this field really doesn't get accessed enough to care, then there's no reason to look at Lazy<T> either, in my opinion.

I agree, but I wanted to get a "fix" in anyway to hopefully show that I'm willing to help address the honest concerns that you bring to the table.

@roji
Copy link

roji commented Apr 23, 2018

re: "just volatile", it's not quite enough: without any locking, multiple threads entering the getter in the "we need to initialize it" path at the same time will race to initialize the result, and different threads might wind up overwriting the instance with a different result; different calls may therefore different instances without something done explicitly to stop that.

I may be wrong, but the code in #39 [...] does lock.

Sorry, I may have misinterpreted your comment. When you said "just volatile", I thought you may have been referring to something other than #39.

Oh, my bad... I think I should have been more explicit. I was only referring to the volatile+locking writes solution in #39.

I don't think such a scenario exists. I believe that #39 is an acceptable solution (or, well, it was when I had reviewed it previously; since then, more lines were changed), and my previous comment said as much. OK, if I'm being completely honest, I was a little annoyed at the changes that weren't related to making the getter avoid a lock in the fast-path, like renames and inlining a method, but I got over that.

I'm with you there - I always prefer PRs (or at least commits) to do just one change.

if you feel this field really doesn't get accessed enough to care, then there's no reason to look at Lazy either, in my opinion.

I agree, but I wanted to get a "fix" in anyway to hopefully show that I'm willing to help address the honest concerns that you bring to the table.

Sure, I really didn't think otherwise at any point - the discussion was valuable and it was interesting to see the benchmarks that came out of it...

I guess that we have the following options at this point:

  1. Merge Improved performance of the Instance property #39, using volatile+lock (and of course we can ask @YohDeadfall to cleanly segregate changes as you said). Unless I'm mistaken, we've addressed concerns that the solution there is complete (answering @FObermaier's comment), if there are still doubts on correctness we can of course look again).
  2. Merge Use Lazy<T> for GeometryServiceProvider.Instance. #40, using Lazy<T>
  3. Not do anything at all.

I personally vote for merging #39 (after changes) due to the reasons given above - it has some non-major advantages over #40 and is just as effective. But ultimately the decision is obviously @FObermaier and @airbreather's...

@airbreather
Copy link
Member Author

Abandoned in favor of #39.

@airbreather airbreather deleted the instance-with-lazy branch April 28, 2018 23:35
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.

None yet

4 participants