0

I have been trying to optimize code to lower my cpu usage. I have rewritten these functions a couple of times to no avail, looking for some assistance.

63% of time spent on for ball in ... line which only has at most 24 children.

   func totalMass() -> CGFloat {
    var ret : CGFloat = 0
    for ball in self.children as! [Ball] {
        ret += ball.mass
    }
    return ret
}

almost 90% of time spent on the if distance(food...) there can be over 800 pieces of 'food' at one time.

func randomMove() {
        confidenceLevel = 0
        if let b = self.children.first as! Ball? {
            if b.physicsBody?.velocity == CGVector(dx: 0, dy: 0) {
                //print("a")
                self.move(randomPosition())
            } else if b.position.x + b.radius > 1950 || b.position.x - b.radius < -1950 {
                //print("here", b.position.x, b.radius)
                self.move(randomPosition())
            } else if b.position.y + b.radius > 1950 || b.position.y - b.radius < -1950 {
                //print("there")
                self.move(randomPosition())
            } else {
                // Keep moving
                let scene : GameScene = self.scene as! GameScene
                for food in scene.foodLayer.children as! [Food] {
                    if distance (food.position, p2: self.centerPosition()) < b.radius * 5 {
                        self.move(food.position)
                        return
                    }
                }
            }
        }
    }

100% of the time spent on the closing curly brace line }) before the third IF statment

override func didSimulatePhysics() {

        world.enumerateChildNodesWithName("//ball*", usingBlock: {
            node, stop in
            let ball = node as! Ball
            ball.regulateSpeed()
            if let body = ball.physicsBody {
                if (body.velocity.speed() > 0.01) {
                    ball.zRotation = body.velocity.angle() - self.offset
                }
            }
        })

        if let p = currentPlayer {
            centerWorldOnPosition(p.centerPosition())
        } else if playerLayer.children.count > 0 {
            let p = playerLayer.children.first! as! Player
            centerWorldOnPosition(p.centerPosition())
        } else {
            centerWorldOnPosition(CGPoint(x: 0, y: 0))
        }
    }
BARIIIIIIICODE
  • 123
  • 1
  • 1
  • 8
  • Print in the middle of what? What else would explain the FPS being 20 and below? – BARIIIIIIICODE Jul 15 '16 at 02:00
  • 1
    Give [*this*](http://stackoverflow.com/a/378024/23771) a try. It will tell you precisely what to look at. – Mike Dunlavey Jul 15 '16 at 02:01
  • Maybe I am missing what you are trying to tell me with that, seems like sampling to discover the problem which is that I have done, or so I think – BARIIIIIIICODE Jul 15 '16 at 02:06
  • to be honest, this code doesnt look very difficult for the cpu to handle, are you using a CADisplayLink with a high frame interval by any chance... although not sure how expensive `world.enumerateChildNodesWithName...` is to run, could be holding the whole show up – Fonix Jul 15 '16 at 03:00
  • `world.enumerateChildNodesWithName` or that how section rather seems to take a bit of CPU usage, but not sure how to rework it. And no to CADisplay – BARIIIIIIICODE Jul 15 '16 at 03:12
  • Nothing thus far has improved it, best I got was 30 fps while profiling with random move running in background but error'd when built to device – BARIIIIIIICODE Jul 15 '16 at 04:09
  • are you using the showPhysics flag on SKView? it is bugged, only turn that on when you need to see the hit boxes – Knight0fDragon Jul 15 '16 at 13:54
  • looks like you need to redo the distance calculations, distance formula is expensive because it uses square root, which is a recursive function. If you do not need the actual distance value, and instead are doing a comparison, use the distance squared. `A^2 + B^2 = C^2`. This will save you a lot of time. So `if (food.position.x - self.centerPosition().x) * (food.position.x - self.centerPosition().x) + (food.position.y - self.centerPosition().y) * (food.position.y - self.centerPosition().y) < ( b.radius * 5) * ( b.radius * 5){` – Knight0fDragon Jul 15 '16 at 16:05
  • @Knight0fDragon The makes it run harder. – BARIIIIIIICODE Jul 15 '16 at 16:43
  • ok probably because of your self.centerPosition. assign that to a variablem and assign your radius squared to a variable outside of the for loop – Knight0fDragon Jul 15 '16 at 16:45
  • I made an answer to help better understand the code – Knight0fDragon Jul 15 '16 at 16:50

2 Answers2

4

You know the old saw about the fisherman? Give him a fish, you feed him for a day. Teach him to fish, you feed him for a lifetime.

Rather than tell you what to fix, let me explain how you can find the speedups, by manual sampling.

Let's suppose your code can be made faster (it probably can). If you do it, it will save some fraction of time, like 20%, say. That means the specific detailed activity that you are going to optimize, no matter what it is, is actually in the process of executing at least 20% of the time.

It could be executing a larger fraction of the time, like 40%. It's just that your optimization may not get rid of all of it. You may only get rid of half of it, so you may only net 20%.

That means if you halt it at random, and examine everything it is doing in detail and why at that point in time, there is a 40% chance you will see it doing the thing you are going to optimize, the wasteful thing.

How do you know if what you see, while wasteful, accounts for enough time to bother fixing? You know, if you see it a second time. Seeing something once doesn't tell you much - only that it takes non-zero time, but the second time you see it, you don't know how big it is, but you do know it's not small. The fewer times you sample before seeing it twice, the bigger it is. Here are the statistics.

So if it's going to save 20%, and if it's executing for 40% of the time, how many samples do you need before seeing it twice? On average, 2/.4, which is 5 samples. If you take 10 samples, how many times can you expect to see it? Four. If you take 10 samples, what is the probability you won't see it more than once? 4.5% If you take 20 samples, what is the probability you will miss it? Practically zero.

So this is the power of manual sampling. You can see the precise detail of what the program is doing and why it's doing it. This is what profilers don't tell you. They take way more samples, and then they mush them together into timing numbers, so you can see so-called "hot code", but you're left guessing what you might do to fix it. Manual examination of a small number of samples, in detail, putting all your programmer intellect into understanding each one, tells you exactly.

That's why this post, while counter to common wisdom, has so many votes. You get more speed out of it.

Community
  • 1
  • 1
Mike Dunlavey
  • 40,059
  • 14
  • 91
  • 135
  • While a wealth of knowledge and do appreciate it, I believe I have done this. I didn't just take one sample of the runnings, I have several and it appears the same functions are getting abused. I have replaced large graphics with smaller removed larger nodes, nothing changed. – BARIIIIIIICODE Jul 15 '16 at 16:45
  • @BARIIIIIIICODE: Did you look at the exact lines being executed in the samples, and the data that they were working on? Was the same thing being done with the same data as at a previous time? Was the `for ball in self.children as! [Ball]` statement itself on the stack in multiple samples? That looks suspicious to me. Possibly unrolling should be considered. This is what you need to do - look at each sample with a suspicious eye. What could be done to reduce/eliminate it? – Mike Dunlavey Jul 15 '16 at 17:22
  • Now I am no expert -- but from what I can see yes. The functions above are vital to the game being called from beginning to end without stopping. the `for ball` is always looking for balls because the total mass needs to be known always. The function for mass when called spends almost all of its time on `for ball` every time. I have tested it many many times. – BARIIIIIIICODE Jul 15 '16 at 17:41
  • @BARIIIIIIICODE: How often does the total mass change? Unless it's really often, you're just repeating the same work over and over, or nearly so. Can you just keep a variable holding the total mass, and then whenever a ball is added, increase the total mass, and when one is removed, decrease it. If ever you're not sure, then re-initialize it by adding them all up. – Mike Dunlavey Jul 15 '16 at 18:07
  • How often it changes varies, it could be every .5 second if there is food around, could stay stagnant if the player hits a dry spot. or increase quite a bit if they consume a large piece of food (50 to 150 for example). But that depends on `totalmass` of said objectThe mass is never really removed except in large chunks never by one. when there is no mass left its death. The games main design is mass. – BARIIIIIIICODE Jul 15 '16 at 18:11
  • @BARIIIIIIICODE: Well, don't you see how keeping track of the total mass in a variable, and only changing it when it needs to be changed, will save you a ton of time compared to constantly recalculating the same number, by adding up the same numbers? – Mike Dunlavey Jul 15 '16 at 18:16
  • 1
    Yes, I am going to try and implement this. Once again thank you for the wealth of knowledge – BARIIIIIIICODE Jul 15 '16 at 18:28
1

Use of the distance formula in this manner can be an expensive operation, If you do not need the actual distance, I would recommend you use distance squared

func randomMove() {
        confidenceLevel = 0
        if let b = self.children.first as! Ball? {
            if b.physicsBody?.velocity == CGVector(dx: 0, dy: 0) {
                //print("a")
                self.move(randomPosition())
            } else if b.position.x + b.radius > 1950 || b.position.x - b.radius < -1950 {
                //print("here", b.position.x, b.radius)
                self.move(randomPosition())
            } else if b.position.y + b.radius > 1950 || b.position.y - b.radius < -1950 {
                //print("there")
                self.move(randomPosition())
            } else {
                // Keep moving
                let bRadiusSqr = b.radius * 5 * b.radius * 5
                let selfPosition = self.centerPosition()
                let scene : GameScene = self.scene as! GameScene
                var moved = false
                scene.foodLayer.children.forEach() {
                    food in
                    if(!moved)
                    {
                        let diffX = food.position.x - selfPosition.x
                        let diffY = food.position.y - selfPosition.y 
                        if diffX * diffX + diffY * diffY < bRadiusSqr  {
                            self.move(food.position)
                            moved = true
                        }
                    }
                }
            }
        }
    }

Going through every food item is a bad way to go, instead give this a try:

func randomMove() {
        confidenceLevel = 0
        if let b = self.children.first as! Ball? {
            if b.physicsBody?.velocity == CGVector(dx: 0, dy: 0) {
                //print("a")
                self.move(randomPosition())
            } else if b.position.x + b.radius > 1950 || b.position.x - b.radius < -1950 {
                //print("here", b.position.x, b.radius)
                self.move(randomPosition())
            } else if b.position.y + b.radius > 1950 || b.position.y - b.radius < -1950 {
                //print("there")
                self.move(randomPosition())
            } else {
                // Keep moving
                let bRadiusSqr = b.radius * 5 * b.radius * 5
                let selfPosition = self.centerPosition()
                let scene : GameScene = self.scene as! GameScene
                let world : SKPhysicsWorld = scene.physicsWorld
                world.enumerateBodiesInRect(CGRectMake(selfPosition.x - b.radius, selfPosition.y - b.radius,bradius*2,bradius*2))
                {
                    body,stop in
                    guard let food = body.node as? Food else { return}                        
                    let diffX = food.position.x - selfPosition.x
                    let diffY = food.position.y - selfPosition.y 
                    if diffX * diffX + diffY * diffY < bRadiusSqr  {
                        self.move(food.position)
                        stop = true
                    }
                }
            }
        }
    }

Unfortunately, our choices are to get bodies in a ray, point, or rectangle, but this will get us all the physics body withing a box of the radius. We use the distance formula afterwards for better accuracy.

Knight0fDragon
  • 16,609
  • 2
  • 23
  • 44