【问题标题】:Numerous short lived tasks slower when multithreaded even with threadpool即使使用线程池,多线程时许多短期任务也会变慢
【发布时间】:2014-02-20 01:40:47
【问题描述】:

背景

我目前有一个线性物理引擎(但这个问题不需要物理引擎知识),其中一部分我正在尝试多线程,希望能提高效率。

这样的部分是宽相位,在这种情况下,这涉及沿所有 3 个轴移动通过所有对象以检查哪些重叠(所有轴上发生的任何重叠都被视为在宽相位中发生碰撞)。 3 轴扫描除了使用普通对象外,完全独立,因此似乎是多线程的好地方。

为了避免线程之间阻塞的可能性,这 3 个进程中的每一个都会在多线程之前(如果适用)获取它想要使用的所有数据的本地副本

虽然这些扫描是一个重要的瓶颈,但它们的寿命很短,但扫描通常会持续 1-4 毫秒。这是一个实时应用程序,其中代码每秒运行 60 次,因此总滴答时间最长为 17 毫秒,所以 1-4 毫秒对我来说是很长的时间。因为这些扫描是短暂的,所以我使用了线程池。特别是Executors.newFixedThreadPool(3),3 轴为 3。

我的测试计算机是具有超线程的双核,因此最多 4 个线程应该很舒服。 使用Runtime.getRuntime().availableProcessors(); 进行检查

问题

在运行以下测试代码时,其中许多短期任务使用线程池运行单线程或多线程,多线程版本要慢得多;查看配置文件数据。即使多线程部分没有共同的对象也是如此。为什么会这样?有什么方法可以同时运行许多短期(1-4 毫秒)任务?

即使将任务做得更大,也只会使多线程版本在性能上接近单线程,并没有像我预期的那样超过它,这让我觉得我做错了什么。

测试代码

public class BroadPhaseAxisSweep implements Callable<Set<PotentialCollisionPrecursor>>  {

    static final int XAXIS=0;
    static final int YAXIS=1;
    static final int ZAXIS=2;

    int axis; 
    int[] axisIndicies;
    boolean[] isStatic;
    boolean[] isLightWeight; 
    boolean[] isCollidable; 

    //orders the same as axisIndicies
    double[] starts;
    double[] ends;

    private static ExecutorService sweepPool = Executors.newFixedThreadPool(3);

    public BroadPhaseAxisSweep(int axis, List<TestObject> allObjects) {
        //all data that will be used by the thread is cached internally to avoid 
        //any concurrent access issues

        this.axis = axis;

        //allObjects is in reality unsorted, axisIndicies holds sorted indices
        //in this case allObjects just "happens" to be already sorted
        this.axisIndicies =new int[allObjects.size()];
        for(int i=0;i<allObjects.size();i++){
            axisIndicies[i]=i;
        }
        isStatic=new boolean[allObjects.size()];
        for(int i=0;i<allObjects.size();i++){
            isStatic[i]=allObjects.get(i).isStatic();
        }
        isLightWeight=new boolean[allObjects.size()];
        for(int i=0;i<allObjects.size();i++){
            isLightWeight[i]=allObjects.get(i).isLightWeightPhysicsObject();
        }
        isCollidable=new boolean[allObjects.size()];
        for(int i=0;i<allObjects.size();i++){
            isCollidable[i]=allObjects.get(i).isCollidable();
        }

        starts=new double[allObjects.size()];
        for(int i=0;i<allObjects.size();i++){
            starts[i]=allObjects.get(i).getStartPoint();
        }
        ends=new double[allObjects.size()];
        for(int i=0;i<allObjects.size();i++){
            ends[i]=allObjects.get(i).getEndPoint();
        }
    }


    @Override
    public Set<PotentialCollisionPrecursor> call() throws Exception {
        return axisSweep_simple(axisIndicies);
    }

    private Set<PotentialCollisionPrecursor> axisSweep_simple(int[] axisIndicies){

        Set<PotentialCollisionPrecursor> thisSweep =new HashSet();


        for(int i=0;i<starts.length;i++){
            if (isCollidable[axisIndicies[i]]){
                double activeObjectEnd=ends[i];
                //sweep forwards until an objects start is before out end
                for(int j=i+1;j<starts.length;j++){
                    //j<startXsIndicies.length is the bare mininmum contrain, most js wont get that far
                    if ((isStatic[axisIndicies[i]]&& isStatic[axisIndicies[j]]) || ((isLightWeight[axisIndicies[i]]&& isLightWeight[axisIndicies[j]]))){
                        //if both objects are static or both are light weight then they cannot by definition collide, we can skip
                        continue;
                    }


                    if (activeObjectEnd>starts[j]){
                        PotentialCollisionPrecursor potentialCollision=new PotentialCollisionPrecursor(getObjectNumberFromAxisNumber(i),getObjectNumberFromAxisNumber(j));
                            thisSweep.add(potentialCollision);
                    }else{
                        break; //this is as far as this active object goes

                    }

                }
            }
        }

        return thisSweep;
    }


    private int getObjectNumberFromAxisNumber(int number){
        return axisIndicies[number];
    }


     public static void main(String[] args){
         int noOfObjectsUnderTest=250;

         List<TestObject> testObjects=new ArrayList<>();

         Random rnd=new Random();
         double runningStartPosition=0;
         for(int i=0;i<noOfObjectsUnderTest;i++){
             runningStartPosition+=rnd.nextDouble()*0.01;
             testObjects.add(new TestObject(runningStartPosition));
         }

         while(true){
             runSingleTreaded(testObjects);
             runMultiThreadedTreaded(testObjects);
         }

     }

    private static void runSingleTreaded(List<TestObject> testObjects) {
        try {
            //XAXIS used over and over again just for test
            Set<PotentialCollisionPrecursor> xSweep=(new BroadPhaseAxisSweep(XAXIS,testObjects)).call();
            Set<PotentialCollisionPrecursor> ySweep=(new BroadPhaseAxisSweep(XAXIS,testObjects)).call();
            Set<PotentialCollisionPrecursor> zSweep=(new BroadPhaseAxisSweep(XAXIS,testObjects)).call();

            System.out.println(xSweep.size()); //just so JIT can't possibly optimise out
            System.out.println(ySweep.size()); //just so JIT can't possibly optimise out
            System.out.println(zSweep.size()); //just so JIT can't possibly optimise out
        } catch (Exception ex) {
            //bad practice, example only
            Logger.getLogger(BroadPhaseAxisSweep.class.getName()).log(Level.SEVERE, null, ex);
        }
    }

    private static void runMultiThreadedTreaded(List<TestObject> testObjects) {
        try {
            //XAXIS used over and over again just for test
            Future<Set<PotentialCollisionPrecursor>> futureX=sweepPool.submit(new BroadPhaseAxisSweep(XAXIS,testObjects));
            Future<Set<PotentialCollisionPrecursor>> futureY=sweepPool.submit(new BroadPhaseAxisSweep(XAXIS,testObjects));
            Future<Set<PotentialCollisionPrecursor>> futureZ=sweepPool.submit(new BroadPhaseAxisSweep(XAXIS,testObjects));

            Set<PotentialCollisionPrecursor> xSweep=futureX.get();
            Set<PotentialCollisionPrecursor> ySweep=futureY.get();
            Set<PotentialCollisionPrecursor> zSweep=futureZ.get();

            System.out.println(xSweep.size()); //just so JIT can't possibly optimise out
            System.out.println(ySweep.size()); //just so JIT can't possibly optimise out
            System.out.println(zSweep.size()); //just so JIT can't possibly optimise out
        } catch (Exception ex) {
            //bad practice, example only
            Logger.getLogger(BroadPhaseAxisSweep.class.getName()).log(Level.SEVERE, null, ex);
        }
    }


    public static class TestObject{

        final boolean isStatic;
        final boolean isLightWeight;
        final boolean isCollidable;
        final double startPointOnAxis;
        final double endPointOnAxis; 

        public TestObject(double startPointOnAxis) {
            Random rnd=new Random();
            this.isStatic = rnd.nextBoolean();
            this.isLightWeight =  rnd.nextBoolean();
            this.isCollidable =  rnd.nextBoolean();
            this.startPointOnAxis = startPointOnAxis;
            this.endPointOnAxis =startPointOnAxis+0.2*rnd.nextDouble();
        }

        public boolean isStatic() {
            return isStatic;
        }

        public boolean isLightWeightPhysicsObject() {
            return isLightWeight;
        }

        public boolean isCollidable() {
            return isCollidable;
        }

        public double getStartPoint() {
            return startPointOnAxis;
        }

        public double getEndPoint() {
            return endPointOnAxis;
        }
    }

}

public class PotentialCollisionPrecursor {
    //holds the object numbers of a potential collision, can be converted to a real PotentialCollision using a list of those objects
    private final int rigidBodyNumber1;
    private final int rigidBodyNumber2; 


    public PotentialCollisionPrecursor(int rigidBodyNumber1, int rigidBodyNumber2) {
        if (rigidBodyNumber1<rigidBodyNumber2){
            this.rigidBodyNumber1 = rigidBodyNumber1;
            this.rigidBodyNumber2 = rigidBodyNumber2;
        }else{
            this.rigidBodyNumber1 = rigidBodyNumber2;
            this.rigidBodyNumber2 = rigidBodyNumber1;
        }
    }

    public int getRigidBodyNumber1() {
        return rigidBodyNumber1;
    }

    public int getRigidBodyNumber2() {
        return rigidBodyNumber2;
    }

    @Override
    public int hashCode() {
        int hash = 7;
        hash = 67 * hash + this.rigidBodyNumber1;
        hash = 67 * hash + this.rigidBodyNumber2;
        return hash;
    }

    @Override
    public boolean equals(Object obj) {
        if (obj == null) {
            return false;
        }
        if (getClass() != obj.getClass()) {
            return false;
        }
        final PotentialCollisionPrecursor other = (PotentialCollisionPrecursor) obj;
        if (this.rigidBodyNumber1 != other.rigidBodyNumber1) {
            return false;
        }
        if (this.rigidBodyNumber2 != other.rigidBodyNumber2) {
            return false;
        }
        return true;
    }

}

不同大小的线程池

在单线程之后,下一个最快的是 2/3 线程池,然后最慢的是线程池中的单个线程(不足为奇,因为它具有所有开销而没有任何收益)

异常大的任务规模

为了测试问题是否只是线程任务太小,我将任务大小增加到大约 100 毫秒。这些给出了更令人困惑的结果; 1 到 3 之间的任意数量的线程速度大致相同,并且比单线程慢

【问题讨论】:

  • 猖獗的猜测 - CPU stalls.
  • 为了测量开销,我会尝试创建一个newFixedThreadPool(1)。也许还有newFixedThreadPool(2),因为超线程并不是性能翻倍。
  • 为什么从不使用 YAXIS 和 ZAXIS?
  • @Alexei 这是我从更大的程序中删除这部分的一部分。在实际版本中使用它们(我可能应该在问题本身中指出 - 不在移动设备上时会这样做)
  • @maaartinus 我已经添加了这些测试,不出所料,多线程“模式”中的单个线程是最慢的,然后 2/3 线程或多或少相同

标签: java multithreading performance threadpool


【解决方案1】:

如果您的广泛扫描只需要几毫秒,那么您最好同步执行所有操作。保留线程池线程(在 Windows 上)所需的工作时间超过 5 毫秒。更不用说您仍然必须在线程之间移动数据并等待上下文切换,然后最终将线程放回您找到它们的位置。

整个过程可能只会降低性能,尤其是因为您正在获取数据的本地副本。如果每次扫描都是自然独立的并且花费超过 500 毫秒,那么您可能会从您实现的某些并发模型中受益。

需要注意的一件有趣的事情是,如今的图形处理器配备了专用于物理计算的嵌入式协处理器。他们如此擅长做这样的事情的原因是因为他们有时有数千个处理器内核都以相对较低的时钟频率运行。这意味着它们非常适合同时处理大量小型任务。您可能想尝试直接与图形处理器接口,以将物理处理卸载到那种环境中,而不是在通用 CPU 上使用它。

【讨论】:

  • 如果有帮助的话,我会多次使用我的线程池(事实上它只创建一次)
  • @RichardTingle 这很好,通常是你做事的方式。每次需要一个新线程时,它都不需要为每个线程分配新的堆栈空间,从而节省了时间。然而,此时的问题并非如此。这主要是因为您过度并行化了一项本身很快的任务。为并发处理做准备的数据副本甚至可能比仅在单个线程上执行任务更昂贵,具体取决于您的集合的分配大小。
  • 图形处理器可能是一个有趣的想法,不久前我尝试了一种天真的方法,但发现将数据传输到 GPU 是主要瓶颈。然而,我当时并不真正知道我在做什么(我是否知道我现在在做什么还有待观察)
  • 出于测试目的,即使在单线程情况下,我也进行了数据复制(尽管这很愚蠢)
  • 一组需要 1-4 毫秒的任务非常值得线程化到池中。用 500 毫秒甚至 5 毫秒向池线程发送信号/将任务排队的想法是荒谬的。 100us 是高估了。你只是在传播线程 FUD。无论 OP 的问题是什么,过短的任务都不是原因。
【解决方案2】:

我答应在这里总结所有的发现......这相当令人困惑,因为主要的罪魁祸首是分析器,它不成比例地减慢了 非主线程到主线程。也许它使用原子计数器来跟踪它的数据,也许它们的开销太高以至于导致这种不合理的结果。

手动测量时间可以得到更好的结果,即多线程加速 30-40%。这是有道理的,因为数据复制会产生很大的顺序开销。

这种复制既没有必要也没有用。这是不必要的,因为所有线程都只读取共享数据。这是没用的,因为读取共享变量并不比读取自己的副本慢:

  • 内核能够非常快速地从彼此的缓存中获取数据
  • 他们将它们的副本放入本地 L1 和 L2 缓存(MESI 协议的“共享”状态)
  • L3 缓存是共享的,这意味着不必要地复制的数据意味着由于更高的内存占用导致更多的 L3 未命中

【讨论】:

  • 在这个程序的真实版本中,手动分析,避免复制使多头版本看起来更好。感谢您的所有帮助
猜你喜欢
  • 1970-01-01
  • 2015-06-26
  • 2011-07-14
  • 1970-01-01
  • 1970-01-01
  • 2023-03-12
  • 1970-01-01
  • 1970-01-01
  • 2012-07-26
相关资源
最近更新 更多