Skip to content

Conversation

@kiszk
Copy link
Member

@kiszk kiszk commented Dec 9, 2017

What changes were proposed in this pull request?

This PR reduce the number of global mutable variables in generated code of SortMergeJoin.

Before this PR, global mutable variables are used to extend lifetime of variables in the nested loop. This can be achieved by declaring variable at the outer most loop level where the variables are used.
In the following example, smj_value8, smj_value8, and smj_value9 are declared as local variable at lines 145-147 in With this PR.

This PR fixes potential assertion error by #19865. Without this PR, a global mutable variable is potentially passed to arguments in generated code of split function.

Without this PR

/* 010 */   int smj_value8;
/* 011 */   boolean smj_value8;
/* 012 */   int smj_value9;
..
/* 143 */   protected void processNext() throws java.io.IOException {
/* 144 */     while (findNextInnerJoinRows(smj_leftInput, smj_rightInput)) {
/* 145 */       boolean smj_loaded = false;
/* 146 */       smj_isNull6 = smj_leftRow.isNullAt(1);
/* 147 */       smj_value9 = smj_isNull6 ? -1 : (smj_leftRow.getInt(1));
/* 148 */       scala.collection.Iterator<UnsafeRow> smj_iterator = smj_matches.generateIterator();
/* 149 */       while (smj_iterator.hasNext()) {
/* 150 */         InternalRow smj_rightRow1 = (InternalRow) smj_iterator.next();
/* 151 */         boolean smj_isNull8 = smj_rightRow1.isNullAt(1);
/* 152 */         int smj_value11 = smj_isNull8 ? -1 : (smj_rightRow1.getInt(1));
/* 153 */
/* 154 */         boolean smj_value12 = (smj_isNull6 && smj_isNull8) ||
/* 155 */         (!smj_isNull6 && !smj_isNull8 && smj_value9 == smj_value11);
/* 156 */         if (false || !smj_value12) continue;
/* 157 */         if (!smj_loaded) {
/* 158 */           smj_loaded = true;
/* 159 */           smj_value8 = smj_leftRow.getInt(0);
/* 160 */         }
/* 161 */         int smj_value10 = smj_rightRow1.getInt(0);
/* 162 */         smj_numOutputRows.add(1);
/* 163 */
/* 164 */         smj_rowWriter.zeroOutNullBytes();
/* 165 */
/* 166 */         smj_rowWriter.write(0, smj_value8);
/* 167 */
/* 168 */         if (smj_isNull6) {
/* 169 */           smj_rowWriter.setNullAt(1);
/* 170 */         } else {
/* 171 */           smj_rowWriter.write(1, smj_value9);
/* 172 */         }
/* 173 */
/* 174 */         smj_rowWriter.write(2, smj_value10);
/* 175 */
/* 176 */         if (smj_isNull8) {
/* 177 */           smj_rowWriter.setNullAt(3);
/* 178 */         } else {
/* 179 */           smj_rowWriter.write(3, smj_value11);
/* 180 */         }
/* 181 */         append(smj_result.copy());
/* 182 */
/* 183 */       }
/* 184 */       if (shouldStop()) return;
/* 185 */     }
/* 186 */   }

With this PR

/* 143 */   protected void processNext() throws java.io.IOException {
/* 144 */     while (findNextInnerJoinRows(smj_leftInput, smj_rightInput)) {
/* 145 */       int smj_value8 = -1;
/* 146 */       boolean smj_isNull6 = false;
/* 147 */       int smj_value9 = -1;
/* 148 */       boolean smj_loaded = false;
/* 149 */       smj_isNull6 = smj_leftRow.isNullAt(1);
/* 150 */       smj_value9 = smj_isNull6 ? -1 : (smj_leftRow.getInt(1));
/* 151 */       scala.collection.Iterator<UnsafeRow> smj_iterator = smj_matches.generateIterator();
/* 152 */       while (smj_iterator.hasNext()) {
/* 153 */         InternalRow smj_rightRow1 = (InternalRow) smj_iterator.next();
/* 154 */         boolean smj_isNull8 = smj_rightRow1.isNullAt(1);
/* 155 */         int smj_value11 = smj_isNull8 ? -1 : (smj_rightRow1.getInt(1));
/* 156 */
/* 157 */         boolean smj_value12 = (smj_isNull6 && smj_isNull8) ||
/* 158 */         (!smj_isNull6 && !smj_isNull8 && smj_value9 == smj_value11);
/* 159 */         if (false || !smj_value12) continue;
/* 160 */         if (!smj_loaded) {
/* 161 */           smj_loaded = true;
/* 162 */           smj_value8 = smj_leftRow.getInt(0);
/* 163 */         }
/* 164 */         int smj_value10 = smj_rightRow1.getInt(0);
/* 165 */         smj_numOutputRows.add(1);
/* 166 */
/* 167 */         smj_rowWriter.zeroOutNullBytes();
/* 168 */
/* 169 */         smj_rowWriter.write(0, smj_value8);
/* 170 */
/* 171 */         if (smj_isNull6) {
/* 172 */           smj_rowWriter.setNullAt(1);
/* 173 */         } else {
/* 174 */           smj_rowWriter.write(1, smj_value9);
/* 175 */         }
/* 176 */
/* 177 */         smj_rowWriter.write(2, smj_value10);
/* 178 */
/* 179 */         if (smj_isNull8) {
/* 180 */           smj_rowWriter.setNullAt(3);
/* 181 */         } else {
/* 182 */           smj_rowWriter.write(3, smj_value11);
/* 183 */         }
/* 184 */         append(smj_result.copy());
/* 185 */
/* 186 */       }
/* 187 */       if (shouldStop()) return;
/* 188 */     }
/* 189 */   }

How was this patch tested?

Existing test cases


s"""
|while (findNextInnerJoinRows($leftInput, $rightInput)) {
| ${leftVarDecl.mkString("\n")}
Copy link
Contributor

Choose a reason for hiding this comment

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

can't we define them before the loop and reuse them?

Copy link
Member Author

@kiszk kiszk Dec 9, 2017

Choose a reason for hiding this comment

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

It could be. Would it be possible to let us know advantages compare to the current method?
I think that to shorten lifetime of variables (i.e. current approach) makes generated code more readable.

Copy link
Member Author

Choose a reason for hiding this comment

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

Since I have no strong preference, I would like to hear opinions from others.

Copy link
Contributor

Choose a reason for hiding this comment

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

I think that the advantage would be to reuse them, avoiding creating and destroying them at every loop.

Copy link
Member Author

@kiszk kiszk Dec 9, 2017

Choose a reason for hiding this comment

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

Since they are local variable, it takes almost no cost in native code. If they are on CPU registers, there is no cost. If they are in stack frame, it is up to one instruction to increase or decrease stack frame size.

WDYT? Did you see huge overhead to create and destroy local variables?

Copy link
Member Author

@kiszk kiszk Dec 10, 2017

Choose a reason for hiding this comment

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

We would appreciate it if you would past the kernel code what you are thinking about.
This is because we would like to measure overhead which you are taking care of.

Copy link
Member Author

Choose a reason for hiding this comment

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

Here is my benchmark result. Is there any your result?

$ cat Loop.java 
public class Loop {
  private static int _i;
  private static boolean _b;
 
  public static void main(String[] args){
    Loop l = new Loop();
    long s, e;
    long c1 = 10000000000L;
    long c2 = 100000L;
    int N = 10;

    // warmup
    for (int i = 0; i < 1000000; i++) {
      l.inner(10000, 100);
      l.outer(10000, 100);
    }

    for (int n = 0; n < N; n++) {
      s = System.nanoTime();
      l.inner(c1, c2);
      e = System.nanoTime();
      System.out.println("inner(ms): " + (e-s) / 1000000);
    }

    for (int n = 0; n < N; n++) {
      s = System.nanoTime();
      l.outer(c1, c2);
      e = System.nanoTime();   
      System.out.println("outer(ms): " + (e-s) / 1000000);
    }
  }
 
  public void inner(long c1, long c2) {
    for (long i = 0; i < c1; i++) {
      int v1 = 1;
      boolean v2 = true;
      for (long j = 0; i < c2; i++) {
        _i = v1;
        _b = v2;
      }
    }
  }
 
  public void outer(long c1, long c2) {
    int v1 = 1;
    boolean v2 = true;
    for (long i = 0; i < c1; i++) {
      for (long j = 0; i < c2; i++) {
        _i = v1;
        _b = v2;
      }
    }
  }
}
$ java -version
openjdk version "1.8.0_131"
OpenJDK Runtime Environment (build 1.8.0_131-8u131-b11-2ubuntu1.16.04.3-b11)
OpenJDK 64-Bit Server VM (build 25.131-b11, mixed mode)
$ java Loop
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
inner(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779
outer(ms): 2779

Copy link
Member

Choose a reason for hiding this comment

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

If no performance difference, we prefer to the simpler codes.

Copy link
Contributor

Choose a reason for hiding this comment

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

Since these local variables are only needed inside the loop, I feel the current code is more readable. Performance is not a concern here as the overhead is very low or none.

Copy link
Contributor

Choose a reason for hiding this comment

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

@kiszk thanks for the test. Then I agree this is the best option, thanks.

@SparkQA
Copy link

SparkQA commented Dec 9, 2017

Test build #84685 has finished for PR 19937 at commit 6d48924.

  • This patch passes all tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@kiszk
Copy link
Member Author

kiszk commented Dec 11, 2017

@cloud-fan @viirya Could you please review this?

* codegen of BoundReference here.
*/
private def createLeftVars(ctx: CodegenContext, leftRow: String): Seq[ExprCode] = {
private def createLeftVars(ctx: CodegenContext, leftRow: String): (Seq[ExprCode], Seq[String]) = {
Copy link
Member

Choose a reason for hiding this comment

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

We need to update the function description.

s"""
|boolean $isNull = false;
|${ctx.javaType(a.dataType)} $value = ${ctx.defaultValue(a.dataType)};
""".stripMargin)
Copy link
Member

Choose a reason for hiding this comment

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

Could you assign it first

val leftVarsDecl = 
...
(ExprCode(code, isNull, value), leftVarsDecl)

Copy link
Contributor

Choose a reason for hiding this comment

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

+1

} else {
ExprCode(s"$value = $valueCode;", "false", value)
(ExprCode(s"$value = $valueCode;", "false", value),
s"""${ctx.javaType(a.dataType)} $value = ${ctx.defaultValue(a.dataType)};""")
Copy link
Member

Choose a reason for hiding this comment

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

The same here.

@cloud-fan
Copy link
Contributor

LGTM except one code style comment

|$value = $isNull ? ${ctx.defaultValue(a.dataType)} : ($valueCode);
""".stripMargin
ExprCode(code, isNull, value)
(ExprCode(code, isNull, value),
Copy link
Member

Choose a reason for hiding this comment

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

Why separate variable declaration and code to two parts? Previously it is because it uses global variables. Now since we use local variables, I think we can simply do:

         val code =
           s"""
              |boolean $isNull = $leftRow.isNullAt($i);
              |${ctx.javaType(a.dataType)} $value = $isNull ? ${ctx.defaultValue(a.dataType)} : ($valueCode);
            """.stripMargin

Don't we?

Copy link
Contributor

Choose a reason for hiding this comment

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

good catch! Makes sense to me.

Copy link
Member Author

@kiszk kiszk Dec 11, 2017

Choose a reason for hiding this comment

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

Good question. If we declare variable types only at code, it may lead to compilation error injanino.

Around here, the code that you pointed out may go to $leftAfter in $condCheck that exists in a if-then block at the inner most of the loop in the generated code, by using leftVar. The variable in leftVars is also referred to at ${consume(ctx, leftVars ++ rightVars)}.

In the following example, If we declare variable types only at code, we will drop lines 145 and will declare int for smj_value8 at lines 162. Since smj_value8 is refered at line 169, the generated code would cause compilation error.

WDYT?

/* 143 */   protected void processNext() throws java.io.IOException {
/* 144 */     while (findNextInnerJoinRows(smj_leftInput, smj_rightInput)) {
/* 145 */       int smj_value8 = -1;
/* 146 */       boolean smj_isNull6 = false;
/* 147 */       int smj_value9 = -1;
/* 148 */       boolean smj_loaded = false;
/* 149 */       smj_isNull6 = smj_leftRow.isNullAt(1);
/* 150 */       smj_value9 = smj_isNull6 ? -1 : (smj_leftRow.getInt(1));
/* 151 */       scala.collection.Iterator<UnsafeRow> smj_iterator = smj_matches.generateIterator();
/* 152 */       while (smj_iterator.hasNext()) {
...
/* 160 */         if (!smj_loaded) {
/* 161 */           smj_loaded = true;
/* 162 */           smj_value8 = smj_leftRow.getInt(0);
/* 163 */         }
/* 164 */         int smj_value10 = smj_rightRow1.getInt(0);
/* 165 */         smj_numOutputRows.add(1);
/* 166 */
/* 167 */         smj_rowWriter.zeroOutNullBytes();
/* 168 */
/* 169 */         smj_rowWriter.write(0, smj_value8);
...
/* 185 */
/* 186 */       }
/* 187 */       if (shouldStop()) return;
/* 188 */     }
/* 189 */   }

Copy link
Member

Choose a reason for hiding this comment

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

Yea, I see, I just ran the test and found it too. Looks like puts the declaration at top is simplest.

@viirya
Copy link
Member

viirya commented Dec 11, 2017

LGTM

@mgaido91
Copy link
Contributor

LGTM too

@SparkQA
Copy link

SparkQA commented Dec 11, 2017

Test build #84703 has finished for PR 19937 at commit 9faf0a2.

  • This patch fails SparkR unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@cloud-fan
Copy link
Contributor

retest this please

@SparkQA
Copy link

SparkQA commented Dec 11, 2017

Test build #84705 has finished for PR 19937 at commit 9faf0a2.

  • This patch fails SparkR unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@cloud-fan
Copy link
Contributor

retest this please

@SparkQA
Copy link

SparkQA commented Dec 11, 2017

Test build #84718 has finished for PR 19937 at commit 9faf0a2.

  • This patch fails SparkR unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@kiszk
Copy link
Member Author

kiszk commented Dec 11, 2017

Jenkins, retest this please

@gatorsmile
Copy link
Member

gatorsmile commented Dec 11, 2017

The test failure is not related to this PR. I have reverted the PR that caused the CRAN checking failure.

@gatorsmile
Copy link
Member

LGTM

Thanks! Merged to master

@asfgit asfgit closed this in c235b5f Dec 11, 2017
@SparkQA
Copy link

SparkQA commented Dec 11, 2017

Test build #84723 has finished for PR 19937 at commit 9faf0a2.

  • This patch fails SparkR unit tests.
  • This patch merges cleanly.
  • This patch adds no public classes.

@kiszk
Copy link
Member Author

kiszk commented Dec 12, 2017

Thank you for pointing out the root cause of this failure.

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.

6 participants