Eugen
Eugen

Reputation: 155

Remove reference of an object while iterating over the array of the objects

I have the example code below. I have a Test object which counts to ten, and then is ready to be destroyed. I have a class which is an wrapper for an array of these objects. This class uses the update method every frame, and iterates through the array of Test objects and update every one. If the current object has reached the maximum count, the reference to the object is removed, and when the iteration is finished, the array becomes compact.

require 'benchmark'
class Test
  def update
    @counter ||= 0
    @counter += 1
  end
  def destroy?
    @counter > 10
  end
  def dispose
    # dispose this object
  end
end

class Tests
  def initialize(number)
    @data = []
    number.times do 
      @data.push(Test.new)
    end
  end
  def add_data(data)
    @data.push(data)
  end
  def update
    @data.each_with_index do |t, index|
      t.update
      if t.destroy?
        t.dispose # need to dispose the sprite object in my realcode before delete ref.
        @data[index] = nil
      end
    end.compact!
  end
  # this would be another attempt, which is a little bit faster
  # but this wont work because in my case the objects I use are
  # sprite objects which needs to be disposed like shown above
  # before the reference is removed
  def update_2
    @data.each(&:update)
    @data.delete_if{|obj| obj.destroy?}
  end
end

iterations = 2000000

Benchmark.bm do |x|
  x.report {Tests.new(iterations).update}
  x.report {Tests.new(iterations).update_2}
end

One problem is that this turns out to be performance heavy if many objects are added. Another problem is that, every frame, another object can be added through the add_data method. I wondering if there could be another approach for all this, to make this be not so heavy to the performance.

edit I use a custom library(http://rmvxace.wikia.com/wiki/RGSS), therefore I edited my code above to make some more things clear. But the main problem stays the update method.

Upvotes: 0

Views: 138

Answers (1)

Myst
Myst

Reputation: 19221

I can see a few performance issues with the code, although others may do better.

Here's what I think:

  1. you are storing nil objects in the Array instead of removing the references and then you are calling the compacts... this iterates over the Array two times instead of one.
  2. your update and delete methods could fit inside one keep_if method call
  3. you would probably do better initializing the object with the initialize method rather then using lazy initialization (though that might or might not show on the benchmarks you created) - you have an extra computation for every update call.

Also, your benchmarks always create an object and update it one time - this is not the best base for comparison.

for instance, your update_2 is actually much slower over time... which you aren't noticing at all with your benchmarks.

I updated your update_2 method and added an update_3 method which was a bit faster with the new benchmarks.

the update_3 method required an adjustment to the Test class, which now has a new method called update_or_destroy and returns true for update and false for destroy.

I also added an initializer method to reduce the lazy initialization effect (during my testing it showed to be an effective way to reduce the workload).

try this:

require 'benchmark'
class Test
  def initialize
    @counter ||= 0
  end
  def update
    @counter += 1
  end
  def destroy?
    @counter > 10
  end
  def dispose
    # dispose this object
  end
  def update_or_destroy
    update
    if destroy?
      dispose
      false
    else
      true
    end
  end
end

class Tests
  def initialize(number)
    @data = []
    number.times do 
      @data.push(Test.new)
    end
  end
  def add_data(data)
    @data.push(data)
  end
  def update
    @data.each_with_index do |t, index|
      t.update
      if t.destroy?
        t.dispose # need to dispose the sprite object in my realcode before delete ref.
        @data[index] = nil
      end
    end.compact!
  end
  # this would be another attempt, which is a little bit faster
  # but this wont work because in my case the objects I use are
  # sprite objects which needs to be disposed like shown above
  # before the reference is removed
  def update_2_updated
    # @data.each(&:update)
    @data.delete_if{|obj| obj.update; obj.destroy?}
  end

  def update_3
    @data.keep_if {|obj| obj.update_or_destroy}
  end
end

iterations = 2000000

Benchmark.bm do |x|
  x.report {Tests.new(iterations).update}
  x.report {Tests.new(iterations).update_2_updated}
  x.report {Tests.new(iterations).update_3}
end

#new benchmarks
iterations = 100000
test_object = Tests.new(iterations)
puts Benchmark.measure { iterations.times { test_object.update  } }

test_object = Tests.new(iterations)
puts Benchmark.measure { iterations.times { test_object.update_2_updated  } }

test_object = Tests.new(iterations)
puts Benchmark.measure { iterations.times { test_object.update_3  } }

Upvotes: 1

Related Questions